Repository: microsoft/edit Branch: main Commit: baa3cb05b473 Files: 96 Total size: 999.2 KB Directory structure: gitextract_909sqtf7/ ├── .cargo/ │ ├── release-nightly.toml │ ├── release-windows-ms.toml │ └── release.toml ├── .devcontainer/ │ └── devcontainer.json ├── .github/ │ └── workflows/ │ ├── ci.yml │ └── winget.yml ├── .gitignore ├── .pipelines/ │ ├── release.yml │ └── tsa.json ├── .vscode/ │ ├── launch.json │ └── tasks.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── SECURITY.md ├── assets/ │ ├── com.microsoft.edit.desktop │ ├── editing-traces/ │ │ ├── README.md │ │ └── rustcode.json.zst │ ├── highlighting-tests/ │ │ └── json.json │ ├── manpage/ │ │ └── edit.1 │ ├── microsoft.sixel │ └── snapcraft.yaml ├── crates/ │ ├── edit/ │ │ ├── Cargo.toml │ │ ├── benches/ │ │ │ └── lib.rs │ │ ├── build/ │ │ │ ├── helpers.rs │ │ │ ├── i18n.rs │ │ │ └── main.rs │ │ └── src/ │ │ ├── base64.rs │ │ ├── bin/ │ │ │ └── edit/ │ │ │ ├── apperr.rs │ │ │ ├── documents.rs │ │ │ ├── draw_editor.rs │ │ │ ├── draw_filepicker.rs │ │ │ ├── draw_menubar.rs │ │ │ ├── draw_statusbar.rs │ │ │ ├── edit.exe.manifest │ │ │ ├── localization.rs │ │ │ ├── main.rs │ │ │ └── state.rs │ │ ├── buffer/ │ │ │ ├── gap_buffer.rs │ │ │ ├── line_cache.rs │ │ │ ├── mod.rs │ │ │ └── navigation.rs │ │ ├── cell.rs │ │ ├── clipboard.rs │ │ ├── document.rs │ │ ├── framebuffer.rs │ │ ├── fuzzy.rs │ │ ├── glob.rs │ │ ├── hash.rs │ │ ├── helpers.rs │ │ ├── icu.rs │ │ ├── input.rs │ │ ├── json.rs │ │ ├── lib.rs │ │ ├── oklab.rs │ │ ├── path.rs │ │ ├── simd/ │ │ │ ├── lines_bwd.rs │ │ │ ├── lines_fwd.rs │ │ │ ├── memchr2.rs │ │ │ └── mod.rs │ │ ├── sys/ │ │ │ ├── mod.rs │ │ │ ├── unix.rs │ │ │ └── windows.rs │ │ ├── tui.rs │ │ ├── unicode/ │ │ │ ├── measurement.rs │ │ │ ├── mod.rs │ │ │ └── tables.rs │ │ └── vt.rs │ ├── stdext/ │ │ ├── Cargo.toml │ │ └── src/ │ │ ├── alloc.rs │ │ ├── arena/ │ │ │ ├── debug.rs │ │ │ ├── fs.rs │ │ │ ├── mod.rs │ │ │ ├── release.rs │ │ │ └── scratch.rs │ │ ├── collections/ │ │ │ ├── mod.rs │ │ │ ├── string.rs │ │ │ └── vec.rs │ │ ├── helpers.rs │ │ ├── lib.rs │ │ ├── simd/ │ │ │ ├── memset.rs │ │ │ └── mod.rs │ │ ├── sys/ │ │ │ ├── mod.rs │ │ │ ├── unix.rs │ │ │ └── windows.rs │ │ └── unicode/ │ │ ├── mod.rs │ │ └── utf8.rs │ └── unicode-gen/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── main.rs │ └── rules.rs ├── i18n/ │ └── edit.toml ├── rust-toolchain.toml └── rustfmt.toml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cargo/release-nightly.toml ================================================ [profile.release] panic = "immediate-abort" [target.'cfg(all(target_os = "windows", target_env = "msvc"))'] rustflags = [ "-Ctarget-feature=+crt-static", "-Clink-args=/DEFAULTLIB:ucrt.lib", "-Clink-args=/NODEFAULTLIB:vcruntime.lib", "-Clink-args=/NODEFAULTLIB:msvcrt.lib", "-Clink-args=/NODEFAULTLIB:libucrt.lib", ] [unstable] panic-immediate-abort = true build-std = ["std", "panic_abort"] ================================================ FILE: .cargo/release-windows-ms.toml ================================================ # 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", "optimize_for_size"] # 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" ================================================ FILE: .cargo/release.toml ================================================ # 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(all(target_os = "windows", target_env = "msvc"))'] rustflags = [ "-Ctarget-feature=+crt-static", "-Clink-args=/DEFAULTLIB:ucrt.lib", "-Clink-args=/NODEFAULTLIB:vcruntime.lib", "-Clink-args=/NODEFAULTLIB:msvcrt.lib", "-Clink-args=/NODEFAULTLIB:libucrt.lib", ] # 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 = ["default", "panic_immediate_abort", "optimize_for_size"] ================================================ FILE: .devcontainer/devcontainer.json ================================================ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/rust { "name": "Rust", "image": "mcr.microsoft.com/devcontainers/rust:1-1-bookworm" } ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - main pull_request: branches: - main env: CARGO_TERM_COLOR: always jobs: check: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: - ubuntu-latest - windows-latest steps: # The Windows runners have autocrlf enabled by default. - name: Disable git autocrlf run: git config --global core.autocrlf false if: matrix.os == 'windows-latest' - name: Checkout uses: actions/checkout@v4 # https://github.com/actions/cache/blob/main/examples.md#rust---cargo # Depends on `Cargo.lock` --> Has to be after checkout. - uses: actions/cache@v4 with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Install Rust run: rustup toolchain install nightly --no-self-update --profile minimal --component rust-src,rustfmt,clippy - name: Check formatting run: cargo fmt --all -- --check - name: Run tests run: cargo test --all-features --all-targets - name: Run clippy run: cargo clippy --all-features --all-targets -- --no-deps --deny warnings ================================================ FILE: .github/workflows/winget.yml ================================================ name: Submit release to the WinGet community repository on: release: types: [published] jobs: publish-winget: name: Submit to WinGet repository # winget-create is only supported on Windows runs-on: windows-latest # winget-create will read the following environment variable to access the GitHub token needed for submitting a PR # See https://aka.ms/winget-create-token env: WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_TOKEN }} # Only submit stable releases if: ${{ !github.event.release.prerelease }} steps: - name: Submit package using wingetcreate run: | # Get installer info from release event $assets = '${{ toJSON(github.event.release.assets) }}' | ConvertFrom-Json $x64InstallerUrl = $assets | Where-Object -Property name -like '*x86_64-windows.zip' | Select-Object -ExpandProperty browser_download_url $arm64InstallerUrl = $assets | Where-Object -Property name -like '*aarch64-windows.zip' | Select-Object -ExpandProperty browser_download_url $packageVersion = (${{ toJSON(github.event.release.tag_name) }}).Trim('v') # Update package using wingetcreate curl.exe -JLO https://aka.ms/wingetcreate/latest .\wingetcreate.exe update Microsoft.Edit ` --version $packageVersion ` --urls $x64InstallerUrl $arm64InstallerUrl ` --submit ================================================ FILE: .gitignore ================================================ .idea .vs *.profraw lcov.info target ================================================ FILE: .pipelines/release.yml ================================================ # 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 - ${{ each platform in parameters.buildPlatforms }}: - pwsh: |- $Dest = New-Item -Type Directory "_staging/${env:RELEASE_NAME}" Write-Host "Staging files from ${env:VPACK_ROOT} at $Dest" Get-ChildItem "${env:VPACK_ROOT}\*" -Include *.exe, *.pdb | Copy-Item -Destination $Dest -Verbose tar.exe -c -v --format=zip -f "$(ob_outputDirectory)\${env:RELEASE_NAME}.zip" -C _staging $env:RELEASE_NAME env: RELEASE_NAME: edit-$(EditVersion)-${{ replace(platform, 'pc-windows-msvc', 'windows') }} ${{ if eq(platform, 'i686-pc-windows-msvc') }}: VPACK_ROOT: "$(ob_createvpack_vpackdirectory)/i386" ${{ elseif eq(platform, 'x86_64-pc-windows-msvc') }}: VPACK_ROOT: "$(ob_createvpack_vpackdirectory)/amd64" ${{ else }}: # aarch64-pc-windows-msvc VPACK_ROOT: "$(ob_createvpack_vpackdirectory)/arm64" displayName: Produce ${{platform}} release archive ================================================ FILE: .pipelines/tsa.json ================================================ { "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" } ================================================ FILE: .vscode/launch.json ================================================ { "version": "0.2.0", "configurations": [ { "name": "Launch edit (Windows)", "preLaunchTask": "rust: cargo build", "type": "cppvsdbg", "request": "launch", "console": "externalTerminal", "program": "${workspaceFolder}/target/debug/edit", "cwd": "${workspaceFolder}", "args": [ "${workspaceFolder}/crates/edit/src/bin/edit/main.rs" ], }, { "name": "Launch edit (GDB, Linux)", "preLaunchTask": "rust: cargo build", "type": "cppdbg", "request": "launch", "miDebuggerPath": "rust-gdb", "externalConsole": true, "program": "${workspaceFolder}/target/debug/edit", "cwd": "${workspaceFolder}", "args": [ "${workspaceFolder}/crates/edit/src/bin/edit/main.rs" ], }, { // NOTE for macOS: In order for this task to work you have to: // 1. Run the "Fix externalConsole on macOS" task once // 2. Add the following to your VS Code settings: // "lldb-dap.environment": { // "LLDB_LAUNCH_FLAG_LAUNCH_IN_TTY": "YES" // } "name": "Launch edit (lldb-dap, macOS)", "preLaunchTask": "rust: cargo build", "type": "lldb-dap", "request": "launch", "program": "${workspaceFolder}/target/debug/edit", "cwd": "${workspaceFolder}", "args": [ "${workspaceFolder}/crates/edit/src/bin/edit/main.rs" ], }, { // This is a workaround for https://github.com/microsoft/vscode-cpptools/issues/5079 "name": "Fix externalConsole on macOS", "type": "node-terminal", "request": "launch", "command": "osascript -e 'tell application \"Terminal\"\ndo script \"echo hello\"\nend tell'" }, ] } ================================================ FILE: .vscode/tasks.json ================================================ { "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" ] } ] } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # 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) ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing ## Translation improvements You can find our translations in [`i18n/edit.toml`](./i18n/edit.toml). Please feel free to open a pull request with your changes at any time. If you'd like to discuss your changes first, please feel free to open an issue. ## 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` ================================================ FILE: Cargo.toml ================================================ [workspace] default-members = ["crates/edit"] members = ["crates/*"] resolver = "2" [workspace.package] edition = "2024" license = "MIT" repository = "https://github.com/microsoft/edit" rust-version = "1.93" # We use `opt-level = "s"` 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 separate *.dwp/*.dSYM so the binary can get stripped strip = "symbols" # See split-debuginfo - allows us to drop the size by ~65% incremental = true # Improves re-compile times [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 [workspace.dependencies] edit = { path = "./crates/edit" } stdext = { path = "./crates/stdext" } unicode-gen = { path = "./crates/unicode-gen" } ================================================ FILE: LICENSE ================================================ 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. ================================================ FILE: README.md ================================================ # ![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 [![Packaging status](https://repology.org/badge/vertical-allrepos/microsoft-edit.svg?exclude_unsupported=1)](https://repology.org/project/microsoft-edit/versions) You can also download binaries from [our Releases page](https://github.com/microsoft/edit/releases/latest). ### Windows You can install the latest version with WinGet: ```powershell winget install Microsoft.Edit ``` ## 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: * Rust 1.90 or earlier: `cargo build --config .cargo/release.toml --release` * otherwise: `cargo build --config .cargo/release-nightly.toml --release` ### Build Configuration During compilation you can set various environment variables to configure the build. The following table lists the available configuration options: Environment variable | Description --- | --- `EDIT_CFG_ICU*` | See [ICU library name (SONAME)](#icu-library-name-soname) for details. `EDIT_CFG_LANGUAGES` | A comma-separated list of languages to include in the build. See [i18n/edit.toml](i18n/edit.toml) for available languages. ## Notes to Package Maintainers ### Package Naming The canonical executable name is "edit" and the alternative name is "msedit". We're aware of the potential conflict of "edit" with existing commands and recommend alternatively naming packages and executables "msedit". Names such as "ms-edit" should be avoided. Assigning an "edit" alias is recommended, if possible. ### ICU library name (SONAME) This project _optionally_ depends on the ICU library for its Search and Replace functionality. By default, the project will look for a SONAME without version suffix: * Windows: `icuuc.dll` * macOS: `libicuuc.dylib` * UNIX, and other OS: `libicuuc.so` If your installation uses a different SONAME, please set the following environment variable at build time: * `EDIT_CFG_ICUUC_SONAME`: For instance, `libicuuc.so.76`. * `EDIT_CFG_ICUI18N_SONAME`: For instance, `libicui18n.so.76`. Additionally, this project assumes that the ICU exports are exported without `_` prefix and without version suffix, such as `u_errorName`. If your installation uses versioned exports, please set: * `EDIT_CFG_ICU_CPP_EXPORTS`: If set to `true`, it'll look for C++ symbols such as `_u_errorName`. Enabled by default on macOS. * `EDIT_CFG_ICU_RENAMING_VERSION`: If set to a version number, such as `76`, it'll look for symbols such as `u_errorName_76`. Finally, you can set the following environment variables: * `EDIT_CFG_ICU_RENAMING_AUTO_DETECT`: If set to `true`, the executable will try to detect the `EDIT_CFG_ICU_RENAMING_VERSION` value at runtime. The way it does this is not officially supported by ICU and as such is not recommended to be relied upon. Enabled by default on UNIX (excluding macOS) if no other options are set. To test your settings, run `cargo test` again but with the `--ignored` flag. For instance: ```sh cargo test -- --ignored ``` ================================================ FILE: SECURITY.md ================================================ ## 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). ================================================ FILE: assets/com.microsoft.edit.desktop ================================================ [Desktop Entry] Type=Application Name=Microsoft Edit GenericName=Text Editor Comment=A simple editor for simple needs Icon=edit Exec=edit %F Terminal=true Categories=Utility;TextEditor; MimeType=text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++; Keywords=text;editor ================================================ FILE: assets/editing-traces/README.md ================================================ # editing-traces This directory contains Seph Gentle's ASCII-only `rustcode` editing traces from: https://github.com/josephg/editing-traces The trace was provided under the [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) license. ================================================ FILE: assets/highlighting-tests/json.json ================================================ { // Object with various value types "string": "Hello, world!", // string literal "numberInt": 42, // integer number "numberFloat": -3.14e+2, // floating point with exponent "booleanTrue": true, // boolean true "booleanFalse": false, // boolean false "nullValue": null, // null literal "array": [ "item1", // string in array 2, // number in array false, // boolean in array null, // null in array { "nested": "object" } // object in array ], "emptyObject": {}, // empty object "emptyArray": [], // empty array /* Multi-line comment: This is a block comment inside JSONC. */ "unicodeString": "Emoji: \uD83D\uDE03", // Unicode escape "escapedChars": "Line1\nLine2\tTabbed\\Backslash\"Quote" // Escaped characters } ================================================ FILE: assets/manpage/edit.1 ================================================ .TH EDIT 1 "version 1.2.1" "December 2025" .SH NAME edit \- a simple text editor .SH SYNOPSIS \fBedit\fP [\fIOPTIONS\fP]... [\fIARGUMENTS\fP]... .SH DESCRIPTION edit is a simple text editor inspired by MS-DOS edit. .SH EDITING Edit is an interactive mode-less editor. Use F10 to access the menus. .SH ARGUMENTS .TP \fIFILE[:LINE[:COLUMN]]\fP The file to open, optionally with line and column (e.g., \fBfoo.txt:123:45\fP). .SH OPTIONS .TP \fB\-h\fP, \fB\-\-help\fP Print the help message. .TP \fB\-v\fP, \fB\-\-version\fP Print the version number. .SH COPYRIGHT Copyright \(co Microsoft Corporation. .br Licensed under the MIT License. .SH SEE ALSO .UR https://github.com/microsoft/edit .UE ================================================ FILE: assets/microsoft.sixel ================================================ 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@\ ================================================ FILE: assets/snapcraft.yaml ================================================ name: msedit base: core24 version: '1.2.1' summary: Edit is an MS-DOS inspired text editor from Microsoft description: | Edit pays homage to the classic MS-DOS Editor, but with a modern interface and input controls similar to VS Code. Learn more at https://github.com/microsoft/edit Disclaimer: This is an unofficial Snap and it is not endorsed by nor affiliated officially with Microsoft Corporation. grade: stable confinement: strict apps: msedit: command: bin/edit plugs: - home parts: edit: source: https://github.com/microsoft/edit.git source-type: git plugin: rust build-packages: - build-essential ================================================ FILE: crates/edit/Cargo.toml ================================================ [package] name = "edit" version = "1.2.1" edition.workspace = true license.workspace = true repository.workspace = true rust-version.workspace = true build = "build/main.rs" categories = ["text-editors"] [[bench]] name = "lib" harness = false [features] # Display editor latency in the top-right corner debug-latency = [] [dependencies] stdext.workspace = true [target.'cfg(unix)'.dependencies] libc = "0.2" [build-dependencies] stdext.workspace = true # The default toml crate bundles its dependencies with bad compile times. Thanks. # Thankfully toml-span exists. FWIW the alternative is yaml-rust (without the 2 suffix). toml-span = { version = "0.6", default-features = false } [target.'cfg(windows)'.build-dependencies] winresource = { version = "0.1", default-features = false } [target.'cfg(windows)'.dependencies.windows-sys] version = "0.61" features = [ "Win32_Globalization", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_LibraryLoader", "Win32_System_Threading", ] [dev-dependencies] criterion = { version = "0.8", features = ["html_reports"] } zstd = { version = "0.13", default-features = false } ================================================ FILE: crates/edit/benches/lib.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use std::hint::black_box; use std::io::Cursor; use std::{mem, vec}; use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; use edit::helpers::*; use edit::{buffer, glob, hash, json, oklab, simd, unicode}; use stdext::arena::{self, scratch_arena}; use stdext::collections::BVec; use stdext::unicode::Utf8Chars; struct EditingTracePatch<'a>(usize, usize, &'a str); struct EditingTraceTransaction<'a> { patches: BVec<'a, EditingTracePatch<'a>>, } struct EditingTraceData<'a> { start_content: &'a str, end_content: &'a str, txns: BVec<'a, EditingTraceTransaction<'a>>, } fn bench_buffer(c: &mut Criterion) { let scratch = scratch_arena(None); let data = { let data = include_bytes!("../../../assets/editing-traces/rustcode.json.zst"); let data = zstd::decode_all(Cursor::new(data)).unwrap(); let data = str::from_utf8(&data).unwrap(); let data = json::parse(&scratch, data).unwrap(); let root = data.as_object().unwrap(); let txns = root.get_array("txns").unwrap(); let mut res = EditingTraceData { start_content: root.get_str("startContent").unwrap(), end_content: root.get_str("endContent").unwrap(), txns: BVec::empty(), }; res.txns.reserve(&*scratch, txns.len()); for txn in txns { let txn = txn.as_object().unwrap(); let patches = txn.get_array("patches").unwrap(); let mut txn = EditingTraceTransaction { patches: BVec::empty() }; txn.patches.reserve(&*scratch, patches.len()); for patch in patches { let patch = patch.as_array().unwrap(); let offset = patch[0].as_number().unwrap() as usize; let del_len = patch[1].as_number().unwrap() as usize; let ins_str = patch[2].as_str().unwrap(); txn.patches.push(&*scratch, EditingTracePatch(offset, del_len, ins_str)); } res.txns.push(&*scratch, txn); } res }; let mut patches_with_coords = Vec::new(); { let mut tb = buffer::TextBuffer::new(false).unwrap(); tb.set_crlf(false); tb.write_raw(data.start_content.as_bytes()); for t in &data.txns { for p in &t.patches { tb.cursor_move_to_offset(p.0); let beg = tb.cursor_logical_pos(); tb.delete(buffer::CursorMovement::Grapheme, p.1 as CoordType); tb.write_raw(p.2.as_bytes()); patches_with_coords.push((beg, p.1 as CoordType, p.2)); } } let mut actual = String::new(); tb.save_as_string(&mut actual); assert_eq!(actual, data.end_content); } let bench_gap_buffer = || { let mut buf = buffer::GapBuffer::new(false).unwrap(); buf.replace(0..usize::MAX, data.start_content.as_bytes()); for t in &data.txns { for p in &t.patches { buf.replace(p.0..p.0 + p.1, p.2.as_bytes()); } } buf }; let bench_text_buffer = || { let mut tb = buffer::TextBuffer::new(false).unwrap(); tb.set_crlf(false); tb.write_raw(data.start_content.as_bytes()); for p in &patches_with_coords { tb.cursor_move_to_logical(p.0); tb.delete(buffer::CursorMovement::Grapheme, p.1); tb.write_raw(p.2.as_bytes()); } tb }; // Sanity check: If this fails, the implementation is incorrect. { let buf = bench_gap_buffer(); let mut actual = Vec::new(); buf.extract_raw(0..usize::MAX, &mut actual, 0); assert_eq!(actual, data.end_content.as_bytes()); } { let mut tb = bench_text_buffer(); let mut actual = String::new(); tb.save_as_string(&mut actual); assert_eq!(actual, data.end_content); } c.benchmark_group("buffer") .bench_function(BenchmarkId::new("GapBuffer", "rustcode"), |b| { b.iter(bench_gap_buffer); }) .bench_function(BenchmarkId::new("TextBuffer", "rustcode"), |b| { b.iter(bench_text_buffer); }); } fn bench_glob(c: &mut Criterion) { // Same benchmark as in glob-match const PATH: &str = "foo/bar/foo/bar/foo/bar/foo/bar/foo/bar.txt"; const GLOB: &str = "foo/**/bar.txt"; c.benchmark_group("glob") .bench_function("glob_match", |b| b.iter(|| assert!(glob::glob_match(GLOB, PATH)))); } fn bench_hash(c: &mut Criterion) { c.benchmark_group("hash") .throughput(Throughput::Bytes(8)) .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_json(c: &mut Criterion) { let str = include_str!("../../../assets/highlighting-tests/json.json"); c.benchmark_group("json").throughput(Throughput::Bytes(str.len() as u64)).bench_function( "parse", |b| { b.iter(|| { let scratch = scratch_arena(None); let obj = json::parse(&scratch, black_box(str)).unwrap(); black_box(obj); }) }, ); } fn bench_oklab(c: &mut Criterion) { c.benchmark_group("oklab") .bench_function("StraightRgba::as_oklab", |b| { b.iter(|| black_box(oklab::StraightRgba::from_le(0xff212cbe)).as_oklab()) }) .bench_function("StraightRgba::oklab_blend", |b| { b.iter(|| { black_box(oklab::StraightRgba::from_le(0x7f212cbe)) .oklab_blend(black_box(oklab::StraightRgba::from_le(0x7f3aae3f))) }) }); } fn bench_simd_lines_fwd(c: &mut Criterion) { let mut group = c.benchmark_group("simd"); let buf = vec![b'\n'; 128 * MEBI]; for &lines in &[1, 8, 128, KIBI, 128 * KIBI, 128 * MEBI] { group.throughput(Throughput::Bytes(lines as u64)).bench_with_input( BenchmarkId::new("lines_fwd", lines), &lines, |b, &lines| { b.iter(|| simd::lines_fwd(black_box(&buf), 0, 0, lines as CoordType)); }, ); } } fn bench_simd_memchr2(c: &mut Criterion) { let mut group = c.benchmark_group("simd"); let mut buf = vec![0u8; 128 * MEBI + KIBI]; // For small sizes we add a small offset of +8, // to ensure we also benchmark the non-SIMD tail handling. // For large sizes, its relative impact is negligible. for &bytes in &[8usize, 128 + 8, KIBI, 128 * KIBI, 128 * MEBI] { group.throughput(Throughput::Bytes(bytes as u64 + 1)).bench_with_input( BenchmarkId::new("memchr2", bytes), &bytes, |b, &size| { buf.fill(b'a'); buf[size] = b'\n'; b.iter(|| simd::memchr2(b'\n', b'\r', black_box(&buf), 0)); }, ); } } fn bench_simd_memset(c: &mut Criterion) { let mut group = c.benchmark_group("simd"); let name = format!("memset<{}>", std::any::type_name::()); let size = mem::size_of::(); let mut buf: Vec = vec![Default::default(); 128 * MEBI / size]; // For small sizes we add a small offset of +8, // to ensure we also benchmark the non-SIMD tail handling. // For large sizes, its relative impact is negligible. for &bytes in &[8usize, 128 + 8, KIBI, 128 * KIBI, 128 * MEBI] { 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(|| stdext::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(|| Utf8Chars::new(bytes, 0).fold(0u32, |acc, ch| acc.wrapping_add(ch as u32))) }); } fn bench(c: &mut Criterion) { arena::init(128 * MEBI).unwrap(); bench_buffer(c); bench_glob(c); bench_hash(c); bench_json(c); bench_oklab(c); bench_simd_lines_fwd(c); bench_simd_memchr2(c); bench_simd_memset::(c); bench_simd_memset::(c); bench_unicode(c); } criterion_group!(benches, bench); criterion_main!(benches); ================================================ FILE: crates/edit/build/helpers.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use std::env::VarError; pub fn env_opt(name: &str) -> String { match std::env::var(name) { Ok(value) => value, Err(VarError::NotPresent) => String::new(), Err(VarError::NotUnicode(_)) => { panic!("Environment variable `{name}` is not valid Unicode") } } } ================================================ FILE: crates/edit/build/i18n.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use std::collections::{BTreeMap, HashMap, HashSet}; use std::fmt::Write as _; use crate::helpers::env_opt; pub fn generate(definitions: &str) -> String { let i18n = toml_span::parse(definitions).expect("Failed to parse i18n file"); let root = i18n.as_table().unwrap(); let mut languages = Vec::new(); let mut aliases = Vec::new(); let mut translations: BTreeMap> = BTreeMap::new(); for (k, v) in root.iter() { match &k.name[..] { "__default__" => { const ERROR: &str = "i18n: __default__ must be [str]"; languages = Vec::from_iter( v.as_array() .expect(ERROR) .iter() .map(|lang| lang.as_str().expect(ERROR).to_string()), ); } "__alias__" => { const ERROR: &str = "i18n: __alias__ must be str->str"; aliases.extend(v.as_table().expect(ERROR).iter().map(|(alias, lang)| { (alias.to_string(), lang.as_str().expect(ERROR).to_string()) })); } _ => { const ERROR: &str = "i18n: LocId must be str->str"; translations.insert( k.name.to_string(), HashMap::from_iter( v.as_table().expect(ERROR).iter().map(|(k, v)| { (k.name.to_string(), v.as_str().expect(ERROR).to_string()) }), ), ); } } } // Use EDIT_CFG_LANGUAGES for the language list if it is set. if let cfg_languages = env_opt("EDIT_CFG_LANGUAGES") && !cfg_languages.is_empty() { languages = cfg_languages.split(',').map(|lang| lang.to_string()).collect(); } // Ensure English as the fallback language is always present. if !languages.iter().any(|l| l == "en") { languages.push("en".to_string()); } // Normalize language tags for use in source code (i.e. no "-"). for lang in &mut languages { if lang.is_empty() { panic!("i18n: empty language tag"); } for c in unsafe { lang.as_bytes_mut() } { *c = match *c { b'A'..=b'Z' | b'a'..=b'z' => c.to_ascii_lowercase(), b'-' => b'_', b'_' => b'_', _ => panic!("i18n: language tag \"{lang}\" must be [a-zA-Z_-]"), } } } // * Validate that there are no duplicate language tags. // * Validate that all language tags are valid. // * Merge the aliases into the languages list. let mut languages_with_aliases: Vec<_>; { let mut specified = HashSet::new(); for lang in &languages { if !specified.insert(lang.as_str()) { panic!("i18n: duplicate language tag \"{lang}\""); } } let mut available = HashSet::new(); for v in translations.values() { for lang in v.keys() { available.insert(lang.as_str()); } } let mut invalid = Vec::new(); for lang in &languages { if !available.contains(lang.as_str()) { invalid.push(lang.as_str()); } } if !invalid.is_empty() { panic!("i18n: invalid language tags {invalid:?}"); } languages_with_aliases = languages.iter().map(|l| (l.clone(), l.clone())).collect(); for (alias, lang) in aliases { if specified.contains(lang.as_str()) && !specified.contains(alias.as_str()) { languages_with_aliases.push((alias, lang)); } } } // Sort languages by: // - "en" first, because it'll map to `LangId::en == 0`, which is the default. // - then alphabetically // - but tags with subtags (e.g. "zh_hans") before those without (e.g. "zh"). { fn sort(a: &String, b: &String) -> std::cmp::Ordering { match (a == "en", b == "en") { (true, false) => std::cmp::Ordering::Less, (false, true) => std::cmp::Ordering::Greater, _ => { let (a0, a1) = a.split_once('_').unwrap_or((a, "xxxxxx")); let (b0, b1) = b.split_once('_').unwrap_or((b, "xxxxxx")); match a0.cmp(b0) { std::cmp::Ordering::Equal => a1.cmp(b1), ord => ord, } } } } languages.sort_unstable_by(sort); languages_with_aliases.sort_unstable_by(|a, b| sort(&a.0, &b.0)); } let mut out = String::new(); // Generate the source code for the i18n data. { _ = write!( out, "\ // This file is generated by build.rs. Do not edit it manually. #[derive(Clone, Copy, PartialEq, Eq)] pub enum LocId {{", ); for (k, _) in translations.iter() { _ = writeln!(out, " {k},"); } _ = write!( out, "\ }} #[allow(non_camel_case_types)] #[derive(Clone, Copy, PartialEq, Eq)] pub enum LangId {{ ", ); for lang in &languages { _ = writeln!(out, " {lang},"); } _ = write!( out, "\ }} const LANGUAGES: &[(&str, LangId)] = &[ " ); for (alias, lang) in &languages_with_aliases { _ = writeln!(out, " ({alias:?}, LangId::{lang}),"); } _ = write!( out, "\ ]; const TRANSLATIONS: [[&str; {}]; {}] = [ ", translations.len(), languages.len(), ); for lang in &languages { _ = writeln!(out, " ["); for (_, v) in translations.iter() { const DEFAULT: &String = &String::new(); let v = v.get(lang).or_else(|| v.get("en")).unwrap_or(DEFAULT); _ = writeln!(out, " {v:?},"); } _ = writeln!(out, " ],"); } _ = writeln!(out, "];"); } out } ================================================ FILE: crates/edit/build/main.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #![allow(irrefutable_let_patterns)] use crate::helpers::env_opt; mod helpers; mod i18n; #[derive(Clone, Copy, PartialEq, Eq)] enum TargetOs { Windows, MacOS, Unix, } fn main() { stdext::arena::init(128 * 1024 * 1024).unwrap(); let target_os = match env_opt("CARGO_CFG_TARGET_OS").as_str() { "windows" => TargetOs::Windows, "macos" | "ios" => TargetOs::MacOS, _ => TargetOs::Unix, }; compile_i18n(); configure_icu(target_os); #[cfg(windows)] configure_windows_binary(target_os); } fn compile_i18n() { let i18n_path = "../../i18n/edit.toml"; let i18n = std::fs::read_to_string(i18n_path).unwrap(); let contents = i18n::generate(&i18n); let out_dir = env_opt("OUT_DIR"); let path = format!("{out_dir}/i18n_edit.rs"); std::fs::write(&path, contents).unwrap(); println!("cargo::rerun-if-env-changed=EDIT_CFG_LANGUAGES"); println!("cargo::rerun-if-changed={i18n_path}"); } fn configure_icu(target_os: TargetOs) { let icuuc_soname = env_opt("EDIT_CFG_ICUUC_SONAME"); let icui18n_soname = env_opt("EDIT_CFG_ICUI18N_SONAME"); let cpp_exports = env_opt("EDIT_CFG_ICU_CPP_EXPORTS"); let renaming_version = env_opt("EDIT_CFG_ICU_RENAMING_VERSION"); let renaming_auto_detect = env_opt("EDIT_CFG_ICU_RENAMING_AUTO_DETECT"); // If none of the `EDIT_CFG_ICU*` environment variables are set, // we default to enabling `EDIT_CFG_ICU_RENAMING_AUTO_DETECT` on UNIX. // This slightly improves portability at least in the cases where the SONAMEs match our defaults. let renaming_auto_detect = if !renaming_auto_detect.is_empty() { renaming_auto_detect.parse::().unwrap() } else { target_os == TargetOs::Unix && icuuc_soname.is_empty() && icui18n_soname.is_empty() && cpp_exports.is_empty() && renaming_version.is_empty() }; if renaming_auto_detect && !renaming_version.is_empty() { // It makes no sense to specify an explicit version and also ask for auto-detection. panic!( "Either `EDIT_CFG_ICU_RENAMING_AUTO_DETECT` or `EDIT_CFG_ICU_RENAMING_VERSION` must be set, but not both" ); } let icuuc_soname = if !icuuc_soname.is_empty() { &icuuc_soname } else { match target_os { TargetOs::Windows => "icuuc.dll", TargetOs::MacOS => "libicucore.dylib", TargetOs::Unix => "libicuuc.so", } }; let icui18n_soname = if !icui18n_soname.is_empty() { &icui18n_soname } else { match target_os { TargetOs::Windows => "icuin.dll", TargetOs::MacOS => "libicucore.dylib", TargetOs::Unix => "libicui18n.so", } }; let icu_export_prefix = if !cpp_exports.is_empty() && cpp_exports.parse::().unwrap() { "_" } else { "" }; let icu_export_suffix = if !renaming_version.is_empty() { format!("_{renaming_version}") } else { String::new() }; println!("cargo::rerun-if-env-changed=EDIT_CFG_ICUUC_SONAME"); println!("cargo::rustc-env=EDIT_CFG_ICUUC_SONAME={icuuc_soname}"); println!("cargo::rerun-if-env-changed=EDIT_CFG_ICUI18N_SONAME"); println!("cargo::rustc-env=EDIT_CFG_ICUI18N_SONAME={icui18n_soname}"); println!("cargo::rerun-if-env-changed=EDIT_CFG_ICU_EXPORT_PREFIX"); println!("cargo::rustc-env=EDIT_CFG_ICU_EXPORT_PREFIX={icu_export_prefix}"); println!("cargo::rerun-if-env-changed=EDIT_CFG_ICU_EXPORT_SUFFIX"); println!("cargo::rustc-env=EDIT_CFG_ICU_EXPORT_SUFFIX={icu_export_suffix}"); println!("cargo::rerun-if-env-changed=EDIT_CFG_ICU_RENAMING_AUTO_DETECT"); println!("cargo::rustc-check-cfg=cfg(edit_icu_renaming_auto_detect)"); if renaming_auto_detect { println!("cargo::rustc-cfg=edit_icu_renaming_auto_detect"); } } #[cfg(windows)] fn configure_windows_binary(target_os: TargetOs) { if target_os != TargetOs::Windows { return; } let manifest_path = "src/bin/edit/edit.exe.manifest"; let icon_path = "../../assets/edit.ico"; winresource::WindowsResource::new() .set_manifest_file(manifest_path) .set("FileDescription", "Microsoft Edit") .set("LegalCopyright", "© Microsoft Corporation. All rights reserved.") .set_icon(icon_path) .compile() .unwrap(); println!("cargo::rerun-if-changed={manifest_path}"); } ================================================ FILE: crates/edit/src/base64.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! Base64 facilities. use stdext::arena::Arena; use stdext::collections::BString; 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<'a>(arena: &'a Arena, dst: &mut BString<'a>, 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(arena, 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 stdext::arena::scratch_arena; use stdext::collections::BString; use super::encode; #[test] fn test_basic() { let scratch = scratch_arena(None); let enc = |s: &[u8]| { let mut dst = BString::empty(); encode(&scratch, &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="); } } ================================================ FILE: crates/edit/src/bin/edit/apperr.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use std::io; use edit::{buffer, icu}; #[derive(Debug)] pub enum Error { Io(io::Error), Icu(icu::Error), } pub type Result = std::result::Result; impl From for Error { fn from(err: io::Error) -> Self { Self::Io(err) } } impl From for Error { fn from(err: icu::Error) -> Self { Self::Icu(err) } } impl From for Error { fn from(err: buffer::IoError) -> Self { match err { buffer::IoError::Io(e) => Self::Io(e), buffer::IoError::Icu(e) => Self::Icu(e), } } } ================================================ FILE: crates/edit/src/bin/edit/documents.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use std::ffi::OsStr; use std::fs::File; use std::path::{Path, PathBuf}; use std::{fs, io}; use edit::buffer::{RcTextBuffer, TextBuffer}; use edit::helpers::{CoordType, Point}; use edit::{path, sys}; use crate::apperr; use crate::state::DisplayablePathBuf; pub struct Document { pub buffer: RcTextBuffer, pub path: Option, pub dir: Option, pub filename: String, pub file_id: Option, pub new_file_counter: usize, } impl Document { pub fn save(&mut self, new_path: Option) -> 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(None, path) { 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(None, path) { 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::from_path(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: Vec, } impl DocumentManager { #[inline] pub fn len(&self) -> usize { self.list.len() } #[inline] pub fn active(&self) -> Option<&Document> { self.list.last() } #[inline] pub fn active_mut(&mut self) -> Option<&mut Document> { self.list.last_mut() } pub fn update_active bool>(&mut self, mut func: F) -> bool { let Some(idx) = self.list.iter().rposition(&mut func) else { return false; }; // Already active (= last) document matched? Nothing to do. if idx == self.list.len() - 1 { return false; } // Otherwise, move the matched document to the end of the list so it becomes active. // Uses unsafe, because `rotate_left()` is horrendously bad with -Copt-level=s // (it's really almost comical) and I just don't tolerate that. // If I'm dead and you're looking to rewrite this use `list.push(list.remove(idx))`. unsafe { let beg = self.list.as_mut_ptr(); let doc = beg.add(idx); let last = beg.add(self.list.len() - 1); let amount = self.list.len() - idx - 1; let mut temp = std::mem::MaybeUninit::::uninit(); // Make a backup of the document std::ptr::copy_nonoverlapping(doc, temp.as_mut_ptr(), 1); // Shift the rest to the front std::ptr::copy(doc.add(1), doc, amount); // Move the backup to the end std::ptr::copy_nonoverlapping(temp.as_ptr(), last, 1); } true } pub fn remove_active(&mut self) { self.list.pop(); } pub fn add_untitled(&mut self) -> apperr::Result<&mut Document> { let buffer = Self::create_buffer()?; 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); // In the future this could use push_mut, but it's unstable right now. As usual. self.list.push(doc); Ok(self.list.last_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 File::open(&path) { Ok(file) => Some(file), Err(err) if err.kind() == io::ErrorKind::NotFound => None, Err(err) => return Err(err.into()), }; let file_id = if file.is_some() { Some(sys::file_id(file.as_ref(), &path)?) } else { None }; // 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 = Self::create_buffer()?; { if let Some(file) = &mut file { let mut tb = buffer.borrow_mut(); 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); if let Some(active) = self.active() && active.path.is_none() && active.file_id.is_none() && !active.buffer.borrow().is_dirty() { // If the current document is a pristine Untitled document with no // name and no ID, replace it with the new document. self.remove_active(); } self.list.push(doc); Ok(self.list.last_mut().unwrap()) } pub fn reflow_all(&self) { for doc in &self.list { let mut tb = doc.buffer.borrow_mut(); tb.reflow(); } } pub fn open_for_reading(path: &Path) -> apperr::Result { File::open(path).map_err(apperr::Error::from) } pub fn open_for_writing(path: &Path) -> apperr::Result { // Error handling for directory creation and file writing // It is worth doing an existence check because it is significantly // faster than calling mkdir() and letting it fail (at least on Windows). if let Some(parent) = path.parent() && !parent.exists() { fs::create_dir_all(parent)?; } File::create(path).map_err(apperr::Error::from) } fn create_buffer() -> apperr::Result { let buffer = TextBuffer::new_rc(false)?; { let mut tb = buffer.borrow_mut(); tb.set_insert_final_newline(!cfg!(windows)); // As mandated by POSIX. tb.set_margin_enabled(true); tb.set_line_highlight_enabled(true); } Ok(buffer) } // 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) { fn parse(s: &[u8]) -> Option { 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) } fn find_colon_rev(bytes: &[u8], offset: usize) -> Option { (0..offset.min(bytes.len())).rev().find(|&i| bytes[i] == b':') } let bytes = path.as_os_str().as_encoded_bytes(); let colend = match find_colon_rev(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) = find_colon_rev(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) { 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 }))); } } ================================================ FILE: crates/edit/src/bin/edit/draw_editor.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use std::num::ParseIntError; use edit::framebuffer::IndexedColor; use edit::helpers::*; use edit::icu; use edit::input::{kbmod, vk}; use edit::tui::*; use stdext::string_from_utf8_lossy_owned; 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) { if let Err(err) = icu::init() { error_log_add(ctx, state, err.into()); state.wants_search.kind = StateSearchKind::Disabled; return; } let Some(doc) = state.documents.active() else { state.wants_search.kind = StateSearchKind::Hidden; return; }; let mut action = 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 = Some(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 = Some(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 = Some(SearchAction::Replace); } else if ctx.consume_shortcut(kbmod::CTRL_ALT | vk::RETURN) { action = Some(SearchAction::ReplaceAll); } } } } ctx.table_end(); ctx.table_begin("options"); ctx.table_set_cell_gap(Size { width: 2, height: 0 }); { let mut change = false; let mut change_action = Some(SearchAction::Search); ctx.table_next_row(); 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 state.wants_search.kind == StateSearchKind::Replace && ctx.button("replace-all", loc(LocId::SearchReplaceAll), ButtonStyle::default()) { change = true; change_action = Some(SearchAction::ReplaceAll); } if ctx.button("close", loc(LocId::SearchClose), ButtonStyle::default()) { state.wants_search.kind = StateSearchKind::Hidden; } if change { action = change_action; state.wants_search.focus = true; ctx.needs_rerender(); } } ctx.table_end(); } ctx.block_end(); if let Some(action) = action { search_execute(ctx, state, action); } } pub enum SearchAction { Search, Replace, ReplaceAll, } pub fn search_execute(ctx: &mut Context, state: &mut State, action: SearchAction) { let Some(doc) = state.documents.active_mut() else { return; }; state.search_success = match action { 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.as_bytes(), ), SearchAction::ReplaceAll => doc.buffer.borrow_mut().find_and_replace_all( &state.search_needle, state.search_options, state.search_replacement.as_bytes(), ), } .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; 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)); { let contains_focus = ctx.contains_focus(); 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), ButtonStyle::default().accelerator('S'), ) { action = Action::Save; } ctx.inherit_focus(); if ctx.button( "no", loc(LocId::UnsavedChangesDialogNo), ButtonStyle::default().accelerator('N'), ) { action = Action::Discard; } if ctx.button("cancel", loc(LocId::Cancel), ButtonStyle::default()) { action = Action::Cancel; } // Handle accelerator shortcuts if contains_focus { 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(); state.wants_close = false; } Action::Cancel => { state.wants_exit = false; state.wants_close = false; } } ctx.needs_rerender(); } pub fn draw_goto_menu(ctx: &mut Context, state: &mut State) { let mut done = false; if let Some(doc) = state.documents.active_mut() { ctx.modal_begin("goto", loc(LocId::FileGoto)); { if ctx.editline("goto-line", &mut state.goto_target) { state.goto_invalid = false; } if state.goto_invalid { ctx.attr_background_rgba(ctx.indexed(IndexedColor::Red)); ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::BrightWhite)); } ctx.attr_intrinsic_size(Size { width: 24, height: 1 }); ctx.steal_focus(); if ctx.consume_shortcut(vk::RETURN) { match validate_goto_point(&state.goto_target) { Ok(point) => { let mut buf = doc.buffer.borrow_mut(); buf.cursor_move_to_logical(point); buf.make_cursor_visible(); done = true; } Err(_) => state.goto_invalid = true, } ctx.needs_rerender(); } } done |= ctx.modal_end(); } else { done = true; } if done { state.wants_goto = false; state.goto_target.clear(); state.goto_invalid = false; ctx.needs_rerender(); } } fn validate_goto_point(line: &str) -> Result { let mut coords = [0; 2]; let (y, x) = line.split_once(':').unwrap_or((line, "0")); // Using a loop here avoids 2 copies of the str->int code. // This makes the binary more compact. for (i, s) in [x, y].iter().enumerate() { coords[i] = s.parse::()?.saturating_sub(1); } Ok(Point { x: coords[0], y: coords[1] }) } ================================================ FILE: crates/edit/src/bin/edit/draw_filepicker.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use std::cmp::Ordering; use std::fs; use std::path::{Path, PathBuf}; use edit::framebuffer::IndexedColor; use edit::helpers::*; use edit::input::{kbmod, vk}; use edit::tui::*; use edit::{icu, path}; use stdext::arena::scratch_arena; use stdext::collections::BVec; 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 contains_focus = ctx.contains_focus(); 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)); let name_changed = ctx.editline("name", &mut state.file_picker_pending_name); ctx.inherit_focus(); if ctx.contains_focus() { if name_changed && ctx.is_focused() { update_autocomplete_suggestions(state); } } else if !state.file_picker_autocomplete.is_empty() { state.file_picker_autocomplete.clear(); } if !state.file_picker_autocomplete.is_empty() { let bg = ctx.indexed_alpha(IndexedColor::Background, 3, 4); let fg = ctx.contrasted(bg); let focus_list_beg = ctx.is_focused() && ctx.consume_shortcut(vk::DOWN); let focus_list_end = ctx.is_focused() && ctx.consume_shortcut(vk::UP); let mut autocomplete_done = ctx.consume_shortcut(vk::ESCAPE); ctx.list_begin("suggestions"); ctx.attr_float(FloatSpec { anchor: Anchor::Last, gravity_x: 0.0, gravity_y: 0.0, offset_x: 0.0, offset_y: 1.0, }); ctx.attr_border(); ctx.attr_background_rgba(bg); ctx.attr_foreground_rgba(fg); { for (idx, suggestion) in state.file_picker_autocomplete.iter().enumerate() { let sel = ctx.list_item(false, suggestion.as_str()); if sel != ListSelection::Unchanged { state.file_picker_pending_name = suggestion.as_path().into(); } if sel == ListSelection::Activated { autocomplete_done = true; } let is_first = idx == 0; let is_last = idx == state.file_picker_autocomplete.len() - 1; if (is_first && focus_list_beg) || (is_last && focus_list_end) { ctx.list_item_steal_focus(); } else if ctx.is_focused() && ((is_first && ctx.consume_shortcut(vk::UP)) || (is_last && ctx.consume_shortcut(vk::DOWN))) { ctx.toss_focus_up(); } } } ctx.list_end(); // If the user typed something, we want to put focus back into the editline. // TODO: The input should be processed by the editline and not simply get swallowed. if ctx.keyboard_input().is_some() { ctx.set_input_consumed(); autocomplete_done = true; } if autocomplete_done { state.file_picker_autocomplete.clear(); } } 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); } 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_revision); ctx.list_begin("files"); ctx.inherit_focus(); for entries in state.file_picker_entries.as_ref().unwrap() { for entry in entries { match ctx.list_item(false, 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(); } ctx.scrollarea_end(); if contains_focus && (ctx.consume_shortcut(vk::BACK) || ctx.consume_shortcut(kbmod::ALT | vk::UP)) { state.file_picker_pending_name = "..".into(); activated = true; } 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() && path.exists() { 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)); { let contains_focus = ctx.contains_focus(); 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), ButtonStyle::default()); ctx.inherit_focus(); if ctx.button("no", loc(LocId::No), ButtonStyle::default()) { state.file_picker_overwrite_warning = None; } } ctx.table_end(); if contains_focus { 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(); state.file_picker_autocomplete = Default::default(); } } // Returns Some(path) if the path refers to a file. fn draw_file_picker_update_path(state: &mut State) -> Option { let old_path = state.file_picker_pending_dir.as_path(); let path = old_path.join(&state.file_picker_pending_name); let path = path::normalize(&path); let (dir, name) = if path.is_dir() { // If the current path is C:\ and the user selects "..", we want to // navigate to the drive picker. Since `path::normalize` will turn C:\.. into C:\, // we can detect this by checking if the length of the path didn't change. let dir = if cfg!(windows) && state.file_picker_pending_name == Path::new("..") // It's unnecessary to check the contents of the paths. && old_path.as_os_str().len() == path.as_os_str().len() { Path::new("") } else { path.as_path() }; (dir, 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::from_path(dir.to_path_buf()); state.file_picker_pending_dir_revision = state.file_picker_pending_dir_revision.wrapping_add(1); 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(); // ["..", directories, files] let mut dirs_files = [Vec::new(), Vec::new(), Vec::new()]; #[cfg(windows)] if dir.as_os_str().is_empty() { // If the path is empty, we are at the drive picker. // Add all drives as entries. for drive in edit::sys::drives() { dirs_files[1].push(DisplayablePathBuf::from_string(format!("{drive}:\\"))); } state.file_picker_entries = Some(dirs_files); return; } if cfg!(windows) || dir.parent().is_some() { dirs_files[0].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(); let dir = metadata.is_dir() || (metadata.is_symlink() && fs::metadata(entry.path()).is_ok_and(|m| m.is_dir())); let idx = if dir { 1 } else { 2 }; if dir { name.push("/"); } dirs_files[idx].push(DisplayablePathBuf::from(name)); } } } for entries in &mut dirs_files[1..] { entries.sort_unstable_by(|a, b| { let a = a.as_bytes(); let b = b.as_bytes(); icu::compare_strings(a, b) }); } state.file_picker_entries = Some(dirs_files); } #[inline(never)] fn update_autocomplete_suggestions(state: &mut State) { state.file_picker_autocomplete.clear(); if state.file_picker_pending_name.as_os_str().is_empty() { return; } let scratch = scratch_arena(None); let needle = state.file_picker_pending_name.as_os_str().as_encoded_bytes(); let mut matches = Vec::new(); // Using binary search below we'll quickly find the lower bound // of items that match the needle (= share a common prefix). // // The problem is finding the upper bound. Here I'm using a trick: // By appending U+10FFFF (the highest possible Unicode code point) // we create a needle that naturally yields an upper bound. let mut needle_upper_bound = BVec::empty(); needle_upper_bound.reserve(&*scratch, needle.len() + 4); needle_upper_bound.extend_from_slice(&*scratch, needle); needle_upper_bound.extend_from_slice(&*scratch, b"\xf4\x8f\xbf\xbf"); if let Some(dirs_files) = &state.file_picker_entries { 'outer: for entries in &dirs_files[1..] { let lower = entries .binary_search_by(|entry| icu::compare_strings(entry.as_bytes(), needle)) .unwrap_or_else(|i| i); for entry in &entries[lower..] { let haystack = entry.as_bytes(); match icu::compare_strings(haystack, &needle_upper_bound) { Ordering::Less => { matches.push(entry.clone()); if matches.len() >= 5 { break 'outer; // Limit to 5 suggestions } } // We're looking for suggestions, not for matches. Ordering::Equal => {} // No more matches possible. Ordering::Greater => break, } } } } state.file_picker_autocomplete = matches; } ================================================ FILE: crates/edit/src/bin/edit/draw_menubar.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use edit::helpers::*; use edit::input::{kbmod, vk}; use edit::tui::*; use stdext::arena_format; 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); { let contains_focus = ctx.contains_focus(); if ctx.menubar_menu_begin(loc(LocId::File), 'F') { draw_menu_file(ctx, state); } if !contains_focus && ctx.consume_shortcut(vk::F10) { ctx.steal_focus(); } if state.documents.active().is_some() { if 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) { tb.cut(ctx.clipboard_mut()); ctx.needs_rerender(); } if ctx.menubar_menu_button(loc(LocId::EditCopy), 'C', kbmod::CTRL | vk::C) { tb.copy(ctx.clipboard_mut()); ctx.needs_rerender(); } if ctx.menubar_menu_button(loc(LocId::EditPaste), 'P', kbmod::CTRL | vk::V) { tb.paste(ctx.clipboard_ref()); 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), 'L', kbmod::CTRL | vk::R) { state.wants_search.kind = StateSearchKind::Replace; state.wants_search.focus = true; } } if ctx.menubar_menu_button(loc(LocId::EditSelectAll), 'A', kbmod::CTRL | vk::A) { tb.select_all(); ctx.needs_rerender(); } ctx.menubar_menu_end(); } fn draw_menu_view(ctx: &mut Context, state: &mut State) { if let Some(doc) = state.documents.active() { let mut tb = doc.buffer.borrow_mut(); let word_wrap = tb.is_word_wrap_enabled(); // All values on the statusbar are currently document specific. if ctx.menubar_menu_button(loc(LocId::ViewFocusStatusbar), 'S', vk::NULL) { state.wants_statusbar_focus = true; } if ctx.menubar_menu_button(loc(LocId::ViewGoToFile), 'F', kbmod::CTRL | vk::P) { state.wants_go_to_file = true; } if ctx.menubar_menu_button(loc(LocId::FileGoto), 'G', kbmod::CTRL | vk::G) { state.wants_goto = true; } 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 Corporation"); 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), ButtonStyle::default()) { state.wants_about = false; } ctx.inherit_focus(); } ctx.block_end(); } ctx.block_end(); } if ctx.modal_end() { state.wants_about = false; } } ================================================ FILE: crates/edit/src/bin/edit/draw_statusbar.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use edit::framebuffer::{Attributes, IndexedColor}; use edit::fuzzy::score_fuzzy; use edit::helpers::*; use edit::icu; use edit::input::vk; use edit::tui::*; use stdext::arena::scratch_arena; use stdext::arena_format; use stdext::collections::BVec; 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" }, ButtonStyle::default()) { 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(), ButtonStyle::default()); 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), ButtonStyle::default()) { state.wants_encoding_change = StateEncodingChange::Reopen; } ctx.focus_on_first_present(); if ctx.button("convert", loc(LocId::EncodingConvert), ButtonStyle::default()) { 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(), ), ButtonStyle::default(), ); 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.contains_focus() && 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(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", ButtonStyle::default()) { 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_go_to_file |= ctx.button("filename", filename, ButtonStyle::default()); ctx.inherit_focus(); ctx.attr_overflow(Overflow::TruncateMiddle); ctx.attr_position(Position::Right); } ctx.block_end(); } else { state.wants_statusbar_focus = false; state.wants_encoding_picker = false; state.wants_indentation_picker = false; } ctx.table_end(); } pub fn draw_dialog_encoding_change(ctx: &mut Context, state: &mut State) { let encoding = state.documents.active_mut().map_or("", |doc| doc.buffer.borrow().encoding()); 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; let mut done = encoding.is_empty(); ctx.modal_begin( "encode", if reopen { loc(LocId::EncodingReopen) } else { loc(LocId::EncodingConvert) }, ); { ctx.table_begin("encoding-search"); ctx.table_set_columns(&[0, COORD_TYPE_SAFE_MAX]); ctx.table_set_cell_gap(Size { width: 1, height: 0 }); ctx.inherit_focus(); { ctx.table_next_row(); ctx.inherit_focus(); ctx.label("needle-label", loc(LocId::SearchNeedleLabel)); if ctx.editline("needle", &mut state.encoding_picker_needle) { encoding_picker_update_list(state); } ctx.inherit_focus(); } ctx.table_end(); ctx.scrollarea_begin("scrollarea", Size { width, height }); ctx.attr_background_rgba(ctx.indexed_alpha(IndexedColor::Black, 1, 4)); { ctx.list_begin("encodings"); ctx.inherit_focus(); for enc in state .encoding_picker_results .as_deref() .unwrap_or_else(|| icu::get_available_encodings().preferred) { if ctx.list_item(enc.canonical == encoding, enc.label) == ListSelection::Activated { change = Some(enc.canonical); break; } ctx.attr_overflow(Overflow::TruncateTail); } ctx.list_end(); } ctx.scrollarea_end(); } done |= ctx.modal_end(); done |= change.is_some(); if let Some(encoding) = change && let Some(doc) = state.documents.active_mut() { 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); } } if done { state.wants_encoding_change = StateEncodingChange::None; state.encoding_picker_needle.clear(); state.encoding_picker_results = None; ctx.needs_rerender(); } } fn encoding_picker_update_list(state: &mut State) { state.encoding_picker_results = None; let needle = state.encoding_picker_needle.trim_ascii(); if needle.is_empty() { return; } let encodings = icu::get_available_encodings(); let scratch = scratch_arena(None); let mut matches = BVec::empty(); for enc in encodings.all { let local_scratch = scratch_arena(Some(&scratch)); let (score, _) = score_fuzzy(&local_scratch, enc.label, needle, true); if score > 0 { matches.push(&*scratch, (score, *enc)); } } matches.sort_unstable_by_key(|b| std::cmp::Reverse(b.0)); state.encoding_picker_results = Some(Vec::from_iter(matches.iter().map(|(_, enc)| *enc))); } pub fn draw_go_to_file(ctx: &mut Context, state: &mut State) { ctx.modal_begin("go-to-file", loc(LocId::ViewGoToFile)); { 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_go_to_file = false; ctx.needs_rerender(); } ctx.list_end(); } ctx.scrollarea_end(); } if ctx.modal_end() { state.wants_go_to_file = false; } } ================================================ FILE: crates/edit/src/bin/edit/edit.exe.manifest ================================================ true UTF-8 SegmentHeap ================================================ FILE: crates/edit/src/bin/edit/localization.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use edit::sys; use stdext::AsciiStringHelpers as _; use stdext::arena::scratch_arena; include!(concat!(env!("OUT_DIR"), "/i18n_edit.rs")); static mut S_LANG: LangId = LangId::en; pub fn init() { let scratch = scratch_arena(None); let langs = sys::preferred_languages(&scratch); let mut lang = LangId::en; 'outer: for l in langs { for (prefix, id) in LANGUAGES { if l.starts_with_ignore_ascii_case(prefix) { lang = *id; break 'outer; } } } unsafe { S_LANG = lang; } } pub fn loc(id: LocId) -> &'static str { TRANSLATIONS[unsafe { S_LANG as usize }][id as usize] } ================================================ FILE: crates/edit/src/bin/edit/main.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. mod apperr; mod documents; mod draw_editor; mod draw_filepicker; mod draw_menubar; mod draw_statusbar; mod localization; mod state; use std::borrow::Cow; use std::path::Path; use std::time::Duration; use std::{env, process}; use draw_editor::*; use draw_filepicker::*; use draw_menubar::*; use draw_statusbar::*; use edit::framebuffer::{self, IndexedColor}; use edit::helpers::*; use edit::input::{self, kbmod, vk}; use edit::oklab::StraightRgba; use edit::tui::*; use edit::vt::{self, Token}; use edit::{base64, path, sys, unicode}; use localization::*; use state::*; use stdext::arena::{self, Arena, scratch_arena}; use stdext::arena_format; use stdext::collections::{BString, BVec}; #[cfg(target_pointer_width = "32")] const SCRATCH_ARENA_CAPACITY: usize = 128 * MEBI; #[cfg(target_pointer_width = "64")] const SCRATCH_ARENA_CAPACITY: usize = 512 * MEBI; // NOTE: Before our main() gets called, Rust initializes its stdlib. This pulls in the entire // std::io::{stdin, stdout, stderr} machinery, and probably some more, which amounts to about 20KB. // It can technically be avoided nowadays with `#![no_main]`. Maybe a fun project for later? :) fn main() -> process::ExitCode { if cfg!(debug_assertions) { let hook = std::panic::take_hook(); 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!("{}\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(()); } // This will reopen stdin if it's redirected (which may fail) and switch // the terminal to raw mode which prevents the user from pressing Ctrl+C. // `handle_args` may want to print a help message (must not fail), // and reads files (may hang; should be cancelable with Ctrl+C). // As such, we call this after `handle_args`. sys::switch_modes()?; let mut vt_parser = vt::Parser::new(); let mut input_parser = input::Parser::new(); let mut tui = Tui::new()?; let _restore = setup_terminal(&mut tui, &mut state, &mut vt_parser); state.menubar_color_bg = tui.indexed(IndexedColor::Background).oklab_blend(tui.indexed_alpha( IndexedColor::BrightBlue, 1, 2, )); state.menubar_color_fg = tui.contrasted(state.menubar_color_bg); let floater_bg = tui .indexed_alpha(IndexedColor::Background, 2, 3) .oklab_blend(tui.indexed_alpha(IndexedColor::Foreground, 1, 3)); let floater_fg = tui.contrasted(floater_bg); tui.setup_modifier_translations(ModifierTranslations { ctrl: loc(LocId::Ctrl), 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-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); write_terminal_title(&scratch, &mut output, &mut state); if state.osc_clipboard_sync { write_osc_clipboard(&scratch, &mut output, &mut tui, &mut state); } #[cfg(feature = "debug-latency")] { use stdext::arena_write_fmt; // 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 edit::helpers::CoordType - 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(&*scratch, 128); // To avoid moving the cursor, push and pop it onto the VT cursor stack. arena_write_fmt!( &*scratch, 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 { let scratch = scratch_arena(None); let mut paths = BVec::empty(); let cwd = env::current_dir()?; let mut dir = None; let mut parse_args = true; // The best CLI argument parser in the world. for arg in env::args_os().skip(1) { if parse_args { if arg == "--" { parse_args = false; continue; } if arg == "-" { paths.clear(); break; } if arg == "-h" || arg == "--help" || (cfg!(windows) && arg == "/?") { print_help(); return Ok(true); } if arg == "-v" || arg == "--version" { print_version(); return Ok(true); } } let p = cwd.join(Path::new(&arg)); let p = path::normalize(&p); if p.is_dir() { state.wants_file_picker = StateFilePicker::Open; dir = Some(p); } else { paths.push(&*scratch, p); } } for p in &paths { state.documents.add_file_path(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 paths.is_empty() { // No files were passed, and stdin is not redirected. state.documents.add_untitled()?; } if dir.is_none() && let Some(parent) = paths.last().and_then(|p| p.parent()) { dir = Some(parent.to_path_buf()); } state.file_picker_pending_dir = DisplayablePathBuf::from_path(dir.unwrap_or(cwd)); Ok(false) } fn print_help() { sys::write_stdout(concat!( "Usage: edit [OPTIONS] [FILE[:LINE[:COLUMN]]]\n", "Options:\n", " -h, --help Print this help message\n", " -v, --version Print the version number\n", "\n", "Arguments:\n", " FILE[:LINE[:COLUMN]] The file to open, optionally with line and column (e.g., foo.txt:123:45)\n", )); } fn print_version() { sys::write_stdout(concat!("edit version ", env!("CARGO_PKG_VERSION"), "\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_goto { draw_goto_menu(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_go_to_file { draw_go_to_file(ctx, state); } if state.wants_about { draw_dialog_about(ctx, state); } if ctx.clipboard_ref().wants_host_sync() { 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_go_to_file = true; } else if key == kbmod::CTRL | vk::Q { state.wants_exit = true; } else if key == kbmod::CTRL | vk::G { state.wants_goto = 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 if key == vk::F3 { search_execute(ctx, state, SearchAction::Search); } 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; } } fn write_terminal_title<'a>(arena: &'a Arena, output: &mut BString<'a>, state: &mut State) { let (filename, dirty) = state .documents .active() .map_or(("", false), |d| (&d.filename, d.buffer.borrow().is_dirty())); if filename == state.osc_title_file_status.filename && dirty == state.osc_title_file_status.dirty { return; } output.push_str(arena, "\x1b]0;"); if !filename.is_empty() { if dirty { output.push_str(arena, "● "); } output.push_str(arena, &sanitize_control_chars(filename)); output.push_str(arena, " - "); } output.push_str(arena, "edit\x1b\\"); state.osc_title_file_status.filename = filename.to_string(); state.osc_title_file_status.dirty = dirty; } const LARGE_CLIPBOARD_THRESHOLD: usize = 128 * KIBI; fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) { let data_len = ctx.clipboard_ref().read().len(); if state.osc_clipboard_always_send || data_len < LARGE_CLIPBOARD_THRESHOLD { ctx.clipboard_mut().mark_as_synchronized(); state.osc_clipboard_sync = true; return; } let over_limit = data_len >= SCRATCH_ARENA_CAPACITY / 4; let mut done = None; 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(data_len)); let mut label = BString::empty(); label.reserve(ctx.arena(), template.len() + size.len()); label.push_str(ctx.arena(), template); label.replace_once_in_place(ctx.arena(), "{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), ButtonStyle::default()) { done = Some(true); } ctx.inherit_focus(); } else { if ctx.button("always", loc(LocId::Always), ButtonStyle::default()) { state.osc_clipboard_always_send = true; done = Some(true); } if ctx.button("yes", loc(LocId::Yes), ButtonStyle::default()) { done = Some(true); } if data_len < 10 * LARGE_CLIPBOARD_THRESHOLD { ctx.inherit_focus(); } if ctx.button("no", loc(LocId::No), ButtonStyle::default()) { done = Some(false); } if data_len >= 10 * LARGE_CLIPBOARD_THRESHOLD { ctx.inherit_focus(); } } } ctx.table_end(); } if ctx.modal_end() { done = Some(false); } if let Some(sync) = done { state.osc_clipboard_sync = sync; ctx.clipboard_mut().mark_as_synchronized(); ctx.needs_rerender(); } } #[cold] fn write_osc_clipboard<'a>( arena: &'a Arena, output: &mut BString<'a>, tui: &mut Tui, state: &mut State, ) { let clipboard = tui.clipboard_mut(); let data = clipboard.read(); if !data.is_empty() { // Rust doubles the size of a string when it needs to grow it. // If `data` 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(arena, base64::encode_len(data.len()) + 16); output.push_str(arena, "\x1b]52;c;"); base64::encode(arena, output, data); output.push_str(arena, "\x1b\\"); } state.osc_clipboard_sync = false; } 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. // We specifically don't reset mode 1036, because most applications expect it to be set nowadays. sys::write_stdout("\x1b[0 q\x1b[?25h\x1b]0;\x07\x1b[?1002;1006;2004l\x1b[?1049l"); } } fn setup_terminal(tui: &mut Tui, state: &mut State, 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 // 1036: Xterm: "meta sends escape" (Alt keypresses should be encoded with ESC + char) "\x1b[?1049h\x1b[?1002;1006;2004h\x1b[?1036h", // 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", // Test whether ambiguous width characters are two columns wide. // We use "…", because it's the most common ambiguous width character we use, // and the old Windows conhost doesn't actually use wcwidth, it measures the // actual display width of the character and assigns it columns accordingly. // We detect it by writing the character and asking for the cursor position. "\r…\x1b[6n", // 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; let mut ambiguous_width = 1; while !done { let scratch = scratch_arena(None); // We explicitly set a high read timeout, because we're not // waiting for user keyboard input. If we encounter a lone ESC, // it's unlikely to be from a ESC keypress, but rather from a VT sequence. let Some(input) = sys::read_stdin(&scratch, Duration::from_secs(3)) else { break; }; let mut vt_stream = vt_parser.parse(&input); while let Some(token) = vt_stream.next() { match token { Token::Csi(csi) => match csi.final_byte { 'c' => done = true, // CPR (Cursor Position Report) response. 'R' => ambiguous_width = csi.params[1] as CoordType - 1, _ => {} }, 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;;rgb://`. "4" => match splits.next().unwrap_or("").parse::() { Ok(val) if val < 16 => &mut indexed_colors[val], _ => continue, }, // The response is `10;rgb://`. "10" => &mut indexed_colors[IndexedColor::Foreground as usize], // The response is `11;rgb://`. "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 = StraightRgba::from_le(rgb | 0xff000000); color_responses += 1; osc_buffer.clear(); } _ => {} } } } if ambiguous_width == 2 { unicode::setup_ambiguous_width(2); state.documents.reflow_all(); } if color_responses == indexed_colors.len() { tui.setup_indexed_colors(indexed_colors); } RestoreModes } /// Strips all C0 control characters from the string and 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) } } ================================================ FILE: crates/edit/src/bin/edit/state.rs ================================================ // 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::framebuffer::IndexedColor; use edit::helpers::*; use edit::oklab::StraightRgba; use edit::tui::*; use edit::{buffer, icu}; use crate::apperr; use crate::documents::DocumentManager; use crate::localization::*; #[repr(transparent)] pub struct FormatApperr(apperr::Error); impl From for FormatApperr { fn from(err: apperr::Error) -> Self { Self(err) } } impl std::fmt::Display for FormatApperr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.0 { apperr::Error::Icu(icu::ICU_MISSING_ERROR) => f.write_str(loc(LocId::ErrorIcuMissing)), apperr::Error::Icu(ref err) => err.fmt(f), apperr::Error::Io(ref err) => err.fmt(f), } } } pub struct DisplayablePathBuf { value: PathBuf, str: Cow<'static, str>, } impl DisplayablePathBuf { #[allow(dead_code, reason = "only used on Windows")] pub fn from_string(string: String) -> Self { let str = Cow::Borrowed(string.as_str()); let str = unsafe { mem::transmute::, Cow<'_, str>>(str) }; let value = PathBuf::from(string); Self { value, str } } pub fn from_path(value: PathBuf) -> Self { let str = value.to_string_lossy(); let str = unsafe { mem::transmute::, 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 { Self::from_path(self.value.clone()) } } impl From for DisplayablePathBuf { fn from(s: OsString) -> Self { Self::from_path(PathBuf::from(s)) } } impl> From<&T> for DisplayablePathBuf { fn from(s: &T) -> Self { Self::from_path(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, } #[derive(Default)] pub struct OscTitleFileStatus { pub filename: String, pub dirty: bool, } pub struct State { pub menubar_color_bg: StraightRgba, pub menubar_color_fg: StraightRgba, 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_dir_revision: u64, // Bumped every time `file_picker_pending_dir` changes. pub file_picker_pending_name: PathBuf, pub file_picker_entries: Option<[Vec; 3]>, // ["..", directories, files] pub file_picker_overwrite_warning: Option, // The path the warning is about. pub file_picker_autocomplete: Vec, pub wants_search: StateSearch, pub search_needle: String, pub search_replacement: String, pub search_options: buffer::SearchOptions, pub search_success: bool, pub wants_encoding_picker: bool, pub wants_encoding_change: StateEncodingChange, pub encoding_picker_needle: String, pub encoding_picker_results: Option>, pub wants_save: bool, pub wants_statusbar_focus: bool, pub wants_indentation_picker: bool, pub wants_go_to_file: bool, pub wants_about: bool, pub wants_close: bool, pub wants_exit: bool, pub wants_goto: bool, pub goto_target: String, pub goto_invalid: bool, pub osc_title_file_status: OscTitleFileStatus, pub osc_clipboard_sync: bool, pub osc_clipboard_always_send: bool, pub exit: bool, } impl State { pub fn new() -> apperr::Result { Ok(Self { menubar_color_bg: StraightRgba::zero(), menubar_color_fg: StraightRgba::zero(), 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_dir_revision: 0, file_picker_pending_name: Default::default(), file_picker_entries: None, file_picker_overwrite_warning: None, file_picker_autocomplete: Vec::new(), wants_search: StateSearch { kind: StateSearchKind::Hidden, focus: false }, search_needle: Default::default(), search_replacement: Default::default(), search_options: Default::default(), search_success: true, wants_encoding_picker: false, encoding_picker_needle: Default::default(), encoding_picker_results: Default::default(), wants_save: false, wants_statusbar_focus: false, wants_encoding_change: StateEncodingChange::None, wants_indentation_picker: false, wants_go_to_file: false, wants_about: false, wants_close: false, wants_exit: false, wants_goto: false, goto_target: Default::default(), goto_invalid: false, osc_title_file_status: Default::default(), osc_clipboard_sync: false, 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), ButtonStyle::default()) { state.error_log_count = 0; } ctx.attr_position(Position::Center); ctx.inherit_focus(); } if ctx.modal_end() { state.error_log_count = 0; } } ================================================ FILE: crates/edit/src/buffer/gap_buffer.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use std::ops::Range; use std::ptr::{self, NonNull}; use std::{io, slice}; use stdext::sys::{virtual_commit, virtual_release, virtual_reserve}; use stdext::{ReplaceRange as _, slice_copy_safe}; use crate::document::{ReadableDocument, WriteableDocument}; use crate::helpers::*; #[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, usize), Vec(Vec), } impl Drop for BackingBuffer { fn drop(&mut self) { unsafe { if let Self::VirtualMemory(ptr, reserve) = *self { virtual_release(ptr, reserve); } } } } /// Most people know how `Vec` 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, /// 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) -> io::Result { 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 { 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 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, 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, range: Range, out: &mut Vec, mut out_off: usize) { let end = range.end.min(self.text_length); let mut beg = range.start.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) } } } ================================================ FILE: crates/edit/src/buffer/line_cache.rs ================================================ use std::ops::Range; use crate::{document::ReadableDocument, simd::memchr2}; /// Cache a line/offset pair every CACHE_EVERY lines to speed up line/offset calculations const CACHE_EVERY: usize = 1024 * 64; #[derive(Clone)] pub struct CachePoint { pub index: usize, pub line: usize, // pub snapshot: ParserSnapshot } pub struct LineCache { cache: Vec, } impl LineCache { pub fn new() -> Self { Self { cache: vec![] } } pub fn from_document(&mut self, document: &T) { self.cache.clear(); let mut offset = 0; let mut line = 0; loop { let text = document.read_forward(offset); if text.is_empty() { return; } let mut off = 0; loop { off = memchr2(b'\n', b'\n', text, off); if off == text.len() { break; } if line % CACHE_EVERY == 0 { self.cache.push(CachePoint { index: offset+off, line }); } line += 1; off += 1; } offset += text.len(); } } /// Updates the cache after a deletion. /// `range` is the deleted byte range, and `text` is the content that was deleted. pub fn delete(&mut self, range: Range, text: &Vec) { let mut newlines = 0; for c in text { if *c == b'\n' { newlines += 1; } } let mut beg_del = None; let mut end_del = None; for (i, point) in self.cache.iter_mut().enumerate() { if point.index >= range.start { if point.index < range.end { // cache point is within the deleted range if beg_del.is_none() { beg_del = Some(i); } end_del = Some(i + 1); } else { point.index -= text.len(); point.line -= newlines; } } } if let (Some(beg), Some(end)) = (beg_del, end_del) { self.cache.drain(beg..end); } } /// Updates the cache after an insertion. /// `offset` is where the insertion occurs, and `text` is the inserted content. pub fn insert(&mut self, offset: usize, text: &[u8]) { // Count how many newlines were inserted let mut newlines = 0; for c in text { if *c == b'\n' { newlines += 1; } } let len = text.len(); for point in &mut self.cache { if point.index > offset { point.index += len; point.line += newlines; } } // TODO: This also needs to insert new cache points } /// Finds the nearest cached line-offset pair relative to a target line. /// If `reverse` is false, it returns the closest *before* the target. /// If `reverse` is true, it returns the closest *after or at* the target. pub fn nearest_offset(&self, target_count: usize, reverse: bool) -> Option { match self.cache.binary_search_by_key(&target_count, |p| p.line) { Ok(i) => Some(self.cache[i].clone()), Err(i) => { if i == 0 || i == self.cache.len() { None } // target < lowest cache point || target > highest cache point else { Some(self.cache[ if reverse {i} else {i-1} ].clone()) } } } } } ================================================ FILE: crates/edit/src/buffer/mod.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! A text buffer for a text editor. //! //! Implements a Unicode-aware, layout-aware text buffer for terminals. //! It's based on a gap buffer. It has no line cache and instead relies //! on the performance of the ucd module for fast text navigation. //! //! --- //! //! If the project ever outgrows a basic gap buffer (e.g. to add time travel) //! an ideal, alternative architecture would be a piece table with immutable trees. //! The tree nodes can be allocated on the same arena allocator as the added chunks, //! making lifetime management fairly easy. The algorithm is described here: //! * //! * //! //! The downside is that text navigation & search takes a performance hit due to small chunks. //! The solution to the former is to keep line caches, which further complicates the architecture. //! There's no solution for the latter. However, there's a chance that the performance will still be sufficient. mod gap_buffer; mod navigation; use std::borrow::Cow; use std::cell::UnsafeCell; use std::collections::VecDeque; use std::fs::File; use std::io::{self, Read as _, Write as _}; use std::mem::{self, MaybeUninit}; use std::ops::Range; use std::rc::Rc; use std::str; pub use gap_buffer::GapBuffer; use stdext::arena::{Arena, scratch_arena}; use stdext::collections::{BString, BVec}; use stdext::unicode::Utf8Chars; use stdext::{ReplaceRange as _, arena_write_fmt, minmax, slice_as_uninit_mut, slice_copy_safe}; use crate::cell::SemiRefCell; use crate::clipboard::Clipboard; use crate::document::{ReadableDocument, WriteableDocument}; use crate::framebuffer::{Framebuffer, IndexedColor}; use crate::helpers::*; use crate::oklab::StraightRgba; use crate::simd::memchr2; use crate::unicode::{self, Cursor, MeasurementConfig}; use crate::{icu, simd}; /// The margin template is used for line numbers. /// The max. line number we should ever expect is probably 64-bit, /// and so this template fits 19 digits, followed by " │ ". const MARGIN_TEMPLATE: &str = " │ "; /// Just a bunch of whitespace you can use for turning tabs into spaces. /// Happens to reuse MARGIN_TEMPLATE, because it has sufficient whitespace. const TAB_WHITESPACE: &str = MARGIN_TEMPLATE; const VISUAL_SPACE: &str = "・"; const VISUAL_SPACE_PREFIX_ADD: usize = '・'.len_utf8() - 1; const VISUAL_TAB: &str = "→ "; const VISUAL_TAB_PREFIX_ADD: usize = '→'.len_utf8() - 1; pub enum IoError { Io(io::Error), Icu(icu::Error), } pub type IoResult = std::result::Result; impl From for IoError { fn from(err: io::Error) -> Self { Self::Io(err) } } impl From for IoError { fn from(err: icu::Error) -> Self { Self::Icu(err) } } /// Stores statistics about the whole document. #[derive(Copy, Clone)] pub struct TextBufferStatistics { logical_lines: CoordType, visual_lines: CoordType, } /// Stores the active text selection anchors. /// /// The two points are not sorted. Instead, `beg` refers to where the selection /// started being made and `end` refers to the currently being updated position. #[derive(Copy, Clone)] struct TextBufferSelection { beg: Point, end: Point, } /// In order to group actions into a single undo step, /// we need to know the type of action that was performed. /// This stores the action type. #[derive(Copy, Clone, Eq, PartialEq)] enum HistoryType { Other, Write, Delete, } /// An undo/redo entry. struct HistoryEntry { /// [`TextBuffer::cursor`] position before the change was made. cursor_before: Point, /// [`TextBuffer::selection`] before the change was made. selection_before: Option, /// [`TextBuffer::stats`] before the change was made. stats_before: TextBufferStatistics, /// [`GapBuffer::generation`] before the change was made. /// /// **NOTE:** Entries with the same generation are grouped together. generation_before: u32, /// Logical cursor position where the change took place. /// The position is at the start of the changed range. cursor: Point, /// Text that was deleted from the buffer. deleted: Vec, /// Text that was added to the buffer. added: Vec, } /// Caches an ICU search operation. struct ActiveSearch { /// The search pattern. pattern: String, /// The search options. options: SearchOptions, /// The ICU `UText` object. text: icu::Text, /// The ICU `URegularExpression` object. regex: icu::Regex, /// [`GapBuffer::generation`] when the search was created. /// This is used to detect if we need to refresh the /// [`ActiveSearch::regex`] object. buffer_generation: u32, /// [`TextBuffer::selection_generation`] when the search was /// created. When the user manually selects text, we need to /// refresh the [`ActiveSearch::pattern`] with it. selection_generation: u32, /// Stores the text buffer offset in between searches. next_search_offset: usize, /// If we know there were no hits, we can skip searching. no_matches: bool, } /// Options for a search operation. #[derive(Default, Clone, Copy, Eq, PartialEq)] pub struct SearchOptions { /// If true, the search is case-sensitive. pub match_case: bool, /// If true, the search matches whole words. pub whole_word: bool, /// If true, the search uses regex. pub use_regex: bool, } enum RegexReplacement<'a> { Group(i32), Text(BVec<'a, u8>), } /// Caches the start and length of the active edit line for a single edit. /// This helps us avoid having to remeasure the buffer after an edit. struct ActiveEditLineInfo { /// Points to the start of the currently being edited line. safe_start: Cursor, /// Number of visual rows of the line that starts /// at [`ActiveEditLineInfo::safe_start`]. line_height_in_rows: CoordType, /// Byte distance from the start of the line at /// [`ActiveEditLineInfo::safe_start`] to the next line. distance_next_line_start: usize, } /// Undo/redo grouping works by recording a set of "overrides", /// which are then applied in [`TextBuffer::edit_begin()`]. /// This allows us to create a group of edits that all share a /// common `generation_before` and can be undone/redone together. /// This struct stores those overrides. struct ActiveEditGroupInfo { /// [`TextBuffer::cursor`] position before the change was made. cursor_before: Point, /// [`TextBuffer::selection`] before the change was made. selection_before: Option, /// [`TextBuffer::stats`] before the change was made. stats_before: TextBufferStatistics, /// [`GapBuffer::generation`] before the change was made. /// /// **NOTE:** Entries with the same generation are grouped together. generation_before: u32, } /// Char- or word-wise navigation? Your choice. pub enum CursorMovement { Grapheme, Word, } /// See [`TextBuffer::move_selected_lines`]. pub enum MoveLineDirection { Up, Down, } /// The result of a call to [`TextBuffer::render()`]. pub struct RenderResult { /// The maximum visual X position we encountered during rendering. pub visual_pos_x_max: CoordType, } /// A [`TextBuffer`] with inner mutability. pub type TextBufferCell = SemiRefCell; /// A [`TextBuffer`] inside an [`Rc`]. /// /// We need this because the TUI system needs to borrow /// the given text buffer(s) until after the layout process. pub type RcTextBuffer = Rc; /// A text buffer for a text editor. pub struct TextBuffer { buffer: GapBuffer, undo_stack: VecDeque>, redo_stack: VecDeque>, last_history_type: HistoryType, last_save_generation: u32, active_edit_group: Option, active_edit_line_info: Option, active_edit_depth: i32, active_edit_off: usize, stats: TextBufferStatistics, cursor: Cursor, // When scrolling significant amounts of text away from the cursor, // rendering will naturally slow down proportionally to the distance. // To avoid this, we cache the cursor position for rendering. // Must be cleared on every edit or reflow. cursor_for_rendering: Option, selection: Option, selection_generation: u32, search: Option>, width: CoordType, margin_width: CoordType, margin_enabled: bool, word_wrap_column: CoordType, word_wrap_enabled: bool, tab_size: CoordType, indent_with_tabs: bool, line_highlight_enabled: bool, ruler: CoordType, encoding: &'static str, newlines_are_crlf: bool, insert_final_newline: bool, overtype: bool, wants_cursor_visibility: bool, } impl TextBuffer { /// Creates a new text buffer inside an [`Rc`]. /// See [`TextBuffer::new()`]. pub fn new_rc(small: bool) -> io::Result { let buffer = Self::new(small)?; Ok(Rc::new(SemiRefCell::new(buffer))) } /// Creates a new text buffer. With `small` you can control /// if the buffer is optimized for <1MiB contents. pub fn new(small: bool) -> io::Result { Ok(Self { buffer: GapBuffer::new(small)?, undo_stack: Default::default(), redo_stack: Default::default(), last_history_type: HistoryType::Other, last_save_generation: 0, active_edit_group: None, active_edit_line_info: None, active_edit_depth: 0, active_edit_off: 0, stats: TextBufferStatistics { logical_lines: 1, visual_lines: 1 }, cursor: Default::default(), cursor_for_rendering: None, selection: None, selection_generation: 0, search: None, width: 0, margin_width: 0, margin_enabled: false, word_wrap_column: 0, word_wrap_enabled: false, tab_size: 4, indent_with_tabs: false, line_highlight_enabled: false, ruler: 0, encoding: "UTF-8", newlines_are_crlf: cfg!(windows), // Windows users want CRLF insert_final_newline: false, overtype: false, wants_cursor_visibility: false, }) } /// Length of the document in bytes. pub fn text_length(&self) -> usize { self.buffer.len() } /// Number of logical lines in the document, /// that is, lines separated by newlines. pub fn logical_line_count(&self) -> CoordType { self.stats.logical_lines } /// Number of visual lines in the document, /// that is, the number of lines after layout. pub fn visual_line_count(&self) -> CoordType { self.stats.visual_lines } /// Does the buffer need to be saved? pub fn is_dirty(&self) -> bool { self.last_save_generation != self.buffer.generation() } /// The buffer generation changes on every edit. /// With this you can check if it has changed since /// the last time you called this function. pub fn generation(&self) -> u32 { self.buffer.generation() } /// Force the buffer to be dirty. pub fn mark_as_dirty(&mut self) { self.last_save_generation = self.buffer.generation().wrapping_sub(1); } fn mark_as_clean(&mut self) { self.last_save_generation = self.buffer.generation(); } /// The encoding used during reading/writing. "UTF-8" is the default. pub fn encoding(&self) -> &'static str { self.encoding } /// Set the encoding used during reading/writing. pub fn set_encoding(&mut self, encoding: &'static str) { if self.encoding != encoding { self.encoding = encoding; self.mark_as_dirty(); } } /// The newline type used in the document. LF or CRLF. pub fn is_crlf(&self) -> bool { self.newlines_are_crlf } /// Changes the newline type without normalizing the document. pub fn set_crlf(&mut self, crlf: bool) { self.newlines_are_crlf = crlf; } /// Changes the newline type used in the document. /// /// NOTE: Cannot be undone. pub fn normalize_newlines(&mut self, crlf: bool) { let newline: &[u8] = if crlf { b"\r\n" } else { b"\n" }; let mut off = 0; let mut cursor_offset = self.cursor.offset; let mut cursor_for_rendering_offset = self.cursor_for_rendering.map_or(cursor_offset, |c| c.offset); #[cfg(debug_assertions)] let mut adjusted_newlines = 0; 'outer: loop { // Seek to the offset of the next line start. loop { let chunk = self.read_forward(off); if chunk.is_empty() { break 'outer; } let (delta, line) = simd::lines_fwd(chunk, 0, 0, 1); off += delta; if line == 1 { break; } } // Get the preceding newline. let chunk = self.read_backward(off); let chunk_newline_len = if chunk.ends_with(b"\r\n") { 2 } else { 1 }; let chunk_newline = &chunk[chunk.len() - chunk_newline_len..]; if chunk_newline != newline { // If this newline is still before our cursor position, then it still has an effect on its offset. // Any newline adjustments past that cursor position are irrelevant. let delta = newline.len() as isize - chunk_newline_len as isize; if off <= cursor_offset { cursor_offset = cursor_offset.saturating_add_signed(delta); #[cfg(debug_assertions)] { adjusted_newlines += 1; } } if off <= cursor_for_rendering_offset { cursor_for_rendering_offset = cursor_for_rendering_offset.saturating_add_signed(delta); } // Replace the newline. off -= chunk_newline_len; self.buffer.replace(off..off + chunk_newline_len, newline); off += newline.len(); } } // If this fails, the cursor offset calculation above is wrong. #[cfg(debug_assertions)] debug_assert_eq!(adjusted_newlines, self.cursor.logical_pos.y); self.cursor.offset = cursor_offset; if let Some(cursor) = &mut self.cursor_for_rendering { cursor.offset = cursor_for_rendering_offset; } self.newlines_are_crlf = crlf; } /// If enabled, automatically insert a final newline /// when typing at the end of the file. pub fn set_insert_final_newline(&mut self, enabled: bool) { self.insert_final_newline = enabled; } /// Whether to insert or overtype text when writing. pub fn is_overtype(&self) -> bool { self.overtype } /// Set the overtype mode. pub fn set_overtype(&mut self, overtype: bool) { self.overtype = overtype; } /// Gets the logical cursor position, that is, /// the position in lines and graphemes per line. pub fn cursor_logical_pos(&self) -> Point { self.cursor.logical_pos } /// Gets the visual cursor position, that is, /// the position in laid out rows and columns. pub fn cursor_visual_pos(&self) -> Point { self.cursor.visual_pos } /// Gets the width of the left margin. pub fn margin_width(&self) -> CoordType { self.margin_width } /// Is the left margin enabled? pub fn set_margin_enabled(&mut self, enabled: bool) -> bool { if self.margin_enabled == enabled { false } else { self.margin_enabled = enabled; self.reflow(); true } } /// Gets the width of the text contents for layout. pub fn text_width(&self) -> CoordType { self.width - self.margin_width } /// Ask the TUI system to scroll the buffer and make the cursor visible. /// /// TODO: This function shows that [`TextBuffer`] is poorly abstracted /// away from the TUI system. The only reason this exists is so that /// if someone outside the TUI code enables word-wrap, the TUI code /// recognizes this and scrolls the cursor into view. But outside of this /// scrolling, views, etc., are all UI concerns = this should not be here. pub fn make_cursor_visible(&mut self) { self.wants_cursor_visibility = true; } /// For the TUI code to retrieve a prior [`TextBuffer::make_cursor_visible()`] request. pub fn take_cursor_visibility_request(&mut self) -> bool { mem::take(&mut self.wants_cursor_visibility) } /// Is word-wrap enabled? /// /// Technically, this is a misnomer, because it's line-wrapping. pub fn is_word_wrap_enabled(&self) -> bool { self.word_wrap_enabled } /// Enable or disable word-wrap. /// /// NOTE: It's expected that the tui code calls `set_width()` sometime after this. /// This will then trigger the actual recalculation of the cursor position. pub fn set_word_wrap(&mut self, enabled: bool) { if self.word_wrap_enabled != enabled { self.word_wrap_enabled = enabled; self.width = 0; // Force a reflow. self.make_cursor_visible(); } } /// Set the width available for layout. /// /// Ideally this would be a pure UI concern, but the text buffer needs this /// so that it can abstract away visual cursor movement such as "go a line up". /// What would that even mean if it didn't know how wide a line is? pub fn set_width(&mut self, width: CoordType) -> bool { if width <= 0 || width == self.width { false } else { self.width = width; self.reflow(); true } } /// Set the tab width. Could be anything, but is expected to be 1-8. pub fn tab_size(&self) -> CoordType { self.tab_size } /// Set the tab size. Clamped to 1-8. pub fn set_tab_size(&mut self, width: CoordType) -> bool { let width = width.clamp(1, 8); if width == self.tab_size { false } else { self.tab_size = width; self.reflow(); true } } /// Calculates the amount of spaces a tab key press would insert at the given column. /// This also equals the visual width of an actual tab character. /// /// This exists because Rust doesn't have range constraints yet, and without /// them assembly blows up in size by 7x. It's a recurring issue with Rust. #[inline] fn tab_size_eval(&self, column: CoordType) -> CoordType { // SAFETY: `set_tab_size` clamps `self.tab_size` to 1-8. unsafe { std::hint::assert_unchecked(self.tab_size >= 1 && self.tab_size <= 8) }; self.tab_size - (column % self.tab_size) } /// If the cursor is at an indentation of `column`, this returns /// the column to which a backspace key press would delete to. #[inline] fn tab_size_prev_column(&self, column: CoordType) -> CoordType { // SAFETY: `set_tab_size` clamps `self.tab_size` to 1-8. unsafe { std::hint::assert_unchecked(self.tab_size >= 1 && self.tab_size <= 8) }; (column - 1).max(0) / self.tab_size * self.tab_size } /// Returns whether tabs are used for indentation. pub fn indent_with_tabs(&self) -> bool { self.indent_with_tabs } /// Sets whether tabs or spaces are used for indentation. pub fn set_indent_with_tabs(&mut self, indent_with_tabs: bool) { self.indent_with_tabs = indent_with_tabs; } /// Sets whether the line the cursor is on should be highlighted. pub fn set_line_highlight_enabled(&mut self, enabled: bool) { self.line_highlight_enabled = enabled; } /// Sets a ruler column, e.g. 80. pub fn set_ruler(&mut self, column: CoordType) { self.ruler = column; } pub fn reflow(&mut self) { self.reflow_internal(true); } fn recalc_after_content_changed(&mut self) { self.reflow_internal(false); } fn reflow_internal(&mut self, force: bool) { let word_wrap_column_before = self.word_wrap_column; { // +1 onto logical_lines, because line numbers are 1-based. // +1 onto log10, because we want the digit width and not the actual log10. // +3 onto log10, because we append " | " to the line numbers to form the margin. self.margin_width = if self.margin_enabled { self.stats.logical_lines.ilog10() as CoordType + 4 } else { 0 }; let text_width = self.text_width(); // 2 columns are required, because otherwise wide glyphs wouldn't ever fit. self.word_wrap_column = if self.word_wrap_enabled && text_width >= 2 { text_width } else { 0 }; } self.cursor_for_rendering = None; if force || self.word_wrap_column != word_wrap_column_before { // Recalculate the cursor position. self.cursor = self.cursor_move_to_logical_internal( if self.word_wrap_column > 0 { Default::default() } else { self.goto_line_start(self.cursor, self.cursor.logical_pos.y) }, self.cursor.logical_pos, ); // Recalculate the line statistics. if self.word_wrap_column > 0 { let end = self.cursor_move_to_logical_internal(self.cursor, Point::MAX); self.stats.visual_lines = end.visual_pos.y + 1; } else { self.stats.visual_lines = self.stats.logical_lines; } } } /// Replaces the entire buffer contents with the given `text`. /// Assumes that the line count doesn't change. pub fn copy_from_str(&mut self, text: &dyn ReadableDocument) { if self.buffer.copy_from(text) { self.recalc_after_content_swap(); self.cursor_move_to_logical(Point { x: CoordType::MAX, y: 0 }); let delete = self.buffer.len() - self.cursor.offset; if delete != 0 { self.buffer.allocate_gap(self.cursor.offset, 0, delete); } } } fn recalc_after_content_swap(&mut self) { // If the buffer was changed, nothing we previously saved can be relied upon. self.undo_stack.clear(); self.redo_stack.clear(); self.last_history_type = HistoryType::Other; self.cursor = Default::default(); self.set_selection(None); self.mark_as_clean(); self.reflow(); } /// Copies the contents of the buffer into a string. pub fn save_as_string(&mut self, dst: &mut dyn WriteableDocument) { self.buffer.copy_into(dst); self.mark_as_clean(); } /// Reads a file from disk into the text buffer, detecting encoding and BOM. pub fn read_file(&mut self, file: &mut File, encoding: Option<&'static str>) -> IoResult<()> { let scratch = scratch_arena(None); let buf = scratch.alloc_uninit_array(); let mut first_chunk_len = 0; let mut read = 0; // Read enough bytes to detect the BOM. while first_chunk_len < BOM_MAX_LEN { read = file_read_uninit(file, &mut buf[first_chunk_len..])?; if read == 0 { break; } first_chunk_len += read; } if let Some(encoding) = encoding { self.encoding = encoding; } else { let bom = detect_bom(unsafe { buf[..first_chunk_len].assume_init_ref() }); self.encoding = bom.unwrap_or("UTF-8"); } // TODO: Since reading the file can fail, we should ensure that we also reset the cursor here. // I don't do it, so that `recalc_after_content_swap()` works. self.buffer.clear(); let done = read == 0; if self.encoding == "UTF-8" { self.read_file_as_utf8(file, buf, first_chunk_len, done)?; } else { self.read_file_with_icu(file, buf, first_chunk_len, done)?; } // Figure out // * the logical line count // * the newline type (LF or CRLF) // * the indentation type (tabs or spaces) // * whether there's a final newline { let chunk = self.read_forward(0); let mut offset = 0; let mut lines = 0; // Number of lines ending in CRLF. let mut crlf_count = 0; // Number of lines starting with a tab. let mut tab_indentations = 0; // Number of lines starting with a space. let mut space_indentations = 0; // Histogram of the indentation depth of lines starting with between 2 and 8 spaces. // In other words, `space_indentation_sizes[0]` is the number of lines starting with 2 spaces. let mut space_indentation_sizes = [0; 7]; loop { // Check if the line starts with a tab. if offset < chunk.len() && chunk[offset] == b'\t' { tab_indentations += 1; } else { // Otherwise, check how many spaces the line starts with. Searching for >8 spaces // allows us to reject lines that have more than 1 level of indentation. let space_indentation = chunk[offset..].iter().take(9).take_while(|&&c| c == b' ').count(); // We'll also reject lines starting with 1 space, because that's too fickle as a heuristic. if (2..=8).contains(&space_indentation) { space_indentations += 1; // If we encounter an indentation depth of 6, it may either be a 6-space indentation, // two 3-space indentation or 3 2-space indentations. To make this work, we increment // all 3 possible histogram slots. // 2 -> 2 // 3 -> 3 // 4 -> 4 2 // 5 -> 5 // 6 -> 6 3 2 // 7 -> 7 // 8 -> 8 4 2 space_indentation_sizes[space_indentation - 2] += 1; if space_indentation & 4 != 0 { space_indentation_sizes[0] += 1; } if space_indentation == 6 || space_indentation == 8 { space_indentation_sizes[space_indentation / 2 - 2] += 1; } } } (offset, lines) = simd::lines_fwd(chunk, offset, lines, lines + 1); // Check if the preceding line ended in CRLF. if offset >= 2 && &chunk[offset - 2..offset] == b"\r\n" { crlf_count += 1; } // We'll limit our heuristics to the first 1000 lines. // That should hopefully be enough in practice. if offset >= chunk.len() || lines >= 1000 { break; } } // We'll assume CRLF if more than half of the lines end in CRLF. If there is only a single line, we'll use the platform default. let newlines_are_crlf = if lines == 0 { cfg!(windows) } else { crlf_count > lines / 2 }; // We'll assume tabs if there are more lines starting with tabs than with spaces. let indent_with_tabs = tab_indentations > space_indentations; let tab_size = if indent_with_tabs { // Tabs will get a visual size of 4 spaces by default. 4 } else { // Otherwise, we'll assume the most common indentation depth. // If there are conflicting indentation depths, we'll prefer the maximum, because in the loop // above we incremented the histogram slot for 2-spaces when encountering 4-spaces and so on. let mut max = 1; let mut tab_size = 4; for (i, &count) in space_indentation_sizes.iter().enumerate() { if count >= max { max = count; tab_size = i as CoordType + 2; } } tab_size }; // If the file has more than 1000 lines, figure out how many are remaining. if offset < chunk.len() { (_, lines) = simd::lines_fwd(chunk, offset, lines, CoordType::MAX); } let final_newline = chunk.ends_with(b"\n"); // Add 1, because the last line doesn't end in a newline (it ends in the literal end). self.stats.logical_lines = lines + 1; self.stats.visual_lines = self.stats.logical_lines; self.newlines_are_crlf = newlines_are_crlf; self.insert_final_newline = final_newline; self.indent_with_tabs = indent_with_tabs; self.tab_size = tab_size; } self.recalc_after_content_swap(); Ok(()) } fn read_file_as_utf8( &mut self, file: &mut File, buf: &mut [MaybeUninit; 4 * KIBI], first_chunk_len: usize, done: bool, ) -> io::Result<()> { { let mut first_chunk = unsafe { buf[..first_chunk_len].assume_init_ref() }; if first_chunk.starts_with(b"\xEF\xBB\xBF") { first_chunk = &first_chunk[3..]; self.encoding = "UTF-8 BOM"; } self.buffer.replace(0..0, first_chunk); } if done { return Ok(()); } // If we don't have file metadata, the input may be a pipe or a socket. // Every read will have the same size until we hit the end. let mut chunk_size = 128 * KIBI; let mut extra_chunk_size = 128 * KIBI; if let Ok(m) = file.metadata() { // Usually the next read of size `chunk_size` will read the entire file, // but if the size has changed for some reason, then `extra_chunk_size` // should be large enough to read the rest of the file. // 4KiB is not too large and not too slow. let len = m.len() as usize; chunk_size = len.saturating_sub(first_chunk_len); extra_chunk_size = 4 * KIBI; } loop { let gap = self.buffer.allocate_gap(self.text_length(), chunk_size, 0); if gap.is_empty() { break; } let read = file.read(gap)?; if read == 0 { break; } self.buffer.commit_gap(read); chunk_size = extra_chunk_size; } Ok(()) } fn read_file_with_icu( &mut self, file: &mut File, buf: &mut [MaybeUninit; 4 * KIBI], first_chunk_len: usize, mut done: bool, ) -> IoResult<()> { let scratch = scratch_arena(None); let pivot_buffer = scratch.alloc_uninit_slice(4 * KIBI); let mut c = icu::Converter::new(pivot_buffer, self.encoding, "UTF-8")?; let mut first_chunk = unsafe { buf[..first_chunk_len].assume_init_ref() }; while !first_chunk.is_empty() { let off = self.text_length(); let gap = self.buffer.allocate_gap(off, 8 * KIBI, 0); let (input_advance, mut output_advance) = c.convert(first_chunk, slice_as_uninit_mut(gap))?; // Remove the BOM from the file, if this is the first chunk. // Our caller ensures to only call us once the BOM has been identified, // which means that if there's a BOM it must be wholly contained in this chunk. if off == 0 { let written = &mut gap[..output_advance]; if written.starts_with(b"\xEF\xBB\xBF") { written.copy_within(3.., 0); output_advance -= 3; } } self.buffer.commit_gap(output_advance); first_chunk = &first_chunk[input_advance..]; } let mut buf_len = 0; loop { if !done { let read = file_read_uninit(file, &mut buf[buf_len..])?; buf_len += read; done = read == 0; } let gap = self.buffer.allocate_gap(self.text_length(), 8 * KIBI, 0); if gap.is_empty() { break; } let read = unsafe { buf[..buf_len].assume_init_ref() }; let (input_advance, output_advance) = c.convert(read, slice_as_uninit_mut(gap))?; self.buffer.commit_gap(output_advance); let flush = done && buf_len == 0; buf_len -= input_advance; buf.copy_within(input_advance.., 0); if flush { break; } } Ok(()) } /// Writes the text buffer contents to a file, handling BOM and encoding. pub fn write_file(&mut self, file: &mut File) -> IoResult<()> { let mut offset = 0; if self.encoding.starts_with("UTF-8") { if self.encoding == "UTF-8 BOM" { file.write_all(b"\xEF\xBB\xBF")?; } loop { let chunk = self.read_forward(offset); if chunk.is_empty() { break; } file.write_all(chunk)?; offset += chunk.len(); } } else { self.write_file_with_icu(file)?; } self.mark_as_clean(); Ok(()) } fn write_file_with_icu(&mut self, file: &mut File) -> IoResult<()> { let scratch = scratch_arena(None); let pivot_buffer = scratch.alloc_uninit_slice(4 * KIBI); let buf = scratch.alloc_uninit_slice(4 * KIBI); let mut c = icu::Converter::new(pivot_buffer, "UTF-8", self.encoding)?; let mut offset = 0; // Write the BOM for the encodings we know need it. if self.encoding.starts_with("UTF-16") || self.encoding.starts_with("UTF-32") || self.encoding == "GB18030" { let (_, output_advance) = c.convert(b"\xEF\xBB\xBF", buf)?; let chunk = unsafe { buf[..output_advance].assume_init_ref() }; file.write_all(chunk)?; } loop { let chunk = self.read_forward(offset); let (input_advance, output_advance) = c.convert(chunk, buf)?; let chunk = unsafe { buf[..output_advance].assume_init_ref() }; file.write_all(chunk)?; offset += input_advance; if chunk.is_empty() { break; } } Ok(()) } /// Returns the current selection. pub fn has_selection(&self) -> bool { self.selection.is_some() } fn set_selection(&mut self, selection: Option) -> u32 { self.selection = selection.filter(|s| s.beg != s.end); self.selection_generation = self.selection_generation.wrapping_add(1); self.selection_generation } /// Moves the cursor by `offset` and updates the selection to contain it. pub fn selection_update_offset(&mut self, offset: usize) { self.set_cursor_for_selection(self.cursor_move_to_offset_internal(self.cursor, offset)); } /// Moves the cursor to `visual_pos` and updates the selection to contain it. pub fn selection_update_visual(&mut self, visual_pos: Point) { self.set_cursor_for_selection(self.cursor_move_to_visual_internal(self.cursor, visual_pos)); } /// Moves the cursor to `logical_pos` and updates the selection to contain it. pub fn selection_update_logical(&mut self, logical_pos: Point) { self.set_cursor_for_selection( self.cursor_move_to_logical_internal(self.cursor, logical_pos), ); } /// Moves the cursor by `delta` and updates the selection to contain it. pub fn selection_update_delta(&mut self, granularity: CursorMovement, delta: CoordType) { self.set_cursor_for_selection(self.cursor_move_delta_internal( self.cursor, granularity, delta, )); } /// Select the current word. pub fn select_word(&mut self) { let Range { start, end } = navigation::word_select(&self.buffer, self.cursor.offset); let beg = self.cursor_move_to_offset_internal(self.cursor, start); let end = self.cursor_move_to_offset_internal(beg, end); unsafe { self.set_cursor(end) }; self.set_selection(Some(TextBufferSelection { beg: beg.logical_pos, end: end.logical_pos, })); } /// Select the current line. pub fn select_line(&mut self) { let beg = self.cursor_move_to_logical_internal( self.cursor, Point { x: 0, y: self.cursor.logical_pos.y }, ); let end = self .cursor_move_to_logical_internal(beg, Point { x: 0, y: self.cursor.logical_pos.y + 1 }); unsafe { self.set_cursor(end) }; self.set_selection(Some(TextBufferSelection { beg: beg.logical_pos, end: end.logical_pos, })); } /// Select the entire document. pub fn select_all(&mut self) { let beg = Default::default(); let end = self.cursor_move_to_logical_internal(beg, Point::MAX); unsafe { self.set_cursor(end) }; self.set_selection(Some(TextBufferSelection { beg: beg.logical_pos, end: end.logical_pos, })); } /// Starts a new selection, if there's none already. pub fn start_selection(&mut self) { if self.selection.is_none() { self.set_selection(Some(TextBufferSelection { beg: self.cursor.logical_pos, end: self.cursor.logical_pos, })); } } /// Destroy the current selection. pub fn clear_selection(&mut self) -> bool { let had_selection = self.selection.is_some(); self.set_selection(None); had_selection } /// Find the next occurrence of the given `pattern` and select it. pub fn find_and_select(&mut self, pattern: &str, options: SearchOptions) -> icu::Result<()> { if let Some(search) = &mut self.search { let search = search.get_mut(); // When the search input changes we must reset the search. if search.pattern != pattern || search.options != options { self.search = None; } // When transitioning from some search to no search, we must clear the selection. if pattern.is_empty() && let Some(TextBufferSelection { beg, .. }) = self.selection { self.cursor_move_to_logical(beg); } } if pattern.is_empty() { return Ok(()); } let search = match &self.search { Some(search) => unsafe { &mut *search.get() }, None => { let search = self.find_construct_search(pattern, options)?; self.search = Some(UnsafeCell::new(search)); unsafe { &mut *self.search.as_ref().unwrap().get() } } }; // If we previously searched through the entire document and found 0 matches, // then we can avoid searching again. if search.no_matches { return Ok(()); } // If the user moved the cursor since the last search, but the needle remained the same, // we still need to move the start of the search to the new cursor position. let next_search_offset = match self.selection { Some(TextBufferSelection { beg, end }) => { if self.selection_generation == search.selection_generation { search.next_search_offset } else { self.cursor_move_to_logical_internal(self.cursor, beg.min(end)).offset } } _ => self.cursor.offset, }; self.find_select_next(search, next_search_offset, true); Ok(()) } /// Find the next occurrence of the given `pattern` and replace it with `replacement`. pub fn find_and_replace( &mut self, pattern: &str, options: SearchOptions, replacement: &[u8], ) -> icu::Result<()> { // Editors traditionally replace the previous search hit, not the next possible one. if let (Some(search), Some(..)) = (&self.search, &self.selection) { let search = unsafe { &mut *search.get() }; if search.selection_generation == self.selection_generation { let scratch = scratch_arena(None); let parsed_replacements = Self::find_parse_replacement(&scratch, &mut *search, replacement); let replacement = self.find_fill_replacement(&mut *search, replacement, &parsed_replacements); self.write(&replacement, self.cursor, true); } } self.find_and_select(pattern, options) } /// Find all occurrences of the given `pattern` and replace them with `replacement`. pub fn find_and_replace_all( &mut self, pattern: &str, options: SearchOptions, replacement: &[u8], ) -> icu::Result<()> { let scratch = scratch_arena(None); let mut search = self.find_construct_search(pattern, options)?; let mut offset = 0; let parsed_replacements = Self::find_parse_replacement(&scratch, &mut search, replacement); loop { self.find_select_next(&mut search, offset, false); if !self.has_selection() { break; } let replacement = self.find_fill_replacement(&mut search, replacement, &parsed_replacements); self.write(&replacement, self.cursor, true); offset = self.cursor.offset; } Ok(()) } fn find_construct_search( &self, pattern: &str, options: SearchOptions, ) -> icu::Result { if pattern.is_empty() { return Err(icu::ILLEGAL_ARGUMENT_ERROR); } let sanitized_pattern = if options.whole_word && options.use_regex { Cow::Owned(format!(r"\b(?:{pattern})\b")) } else if options.whole_word { let mut p = String::with_capacity(pattern.len() + 16); p.push_str(r"\b"); // Escape regex special characters. let b = unsafe { p.as_mut_vec() }; for &byte in pattern.as_bytes() { match byte { b'*' | b'?' | b'+' | b'[' | b'(' | b')' | b'{' | b'}' | b'^' | b'$' | b'|' | b'\\' | b'.' => { b.push(b'\\'); b.push(byte); } _ => b.push(byte), } } p.push_str(r"\b"); Cow::Owned(p) } else { Cow::Borrowed(pattern) }; let mut flags = icu::Regex::MULTILINE; if !options.match_case { flags |= icu::Regex::CASE_INSENSITIVE; } if !options.use_regex && !options.whole_word { flags |= icu::Regex::LITERAL; } // Move the start of the search to the start of the selection, // or otherwise to the current cursor position. let text = unsafe { icu::Text::new(self)? }; let regex = unsafe { icu::Regex::new(&sanitized_pattern, flags, &text)? }; Ok(ActiveSearch { pattern: pattern.to_string(), options, text, regex, buffer_generation: self.buffer.generation(), selection_generation: 0, next_search_offset: 0, no_matches: false, }) } fn find_select_next(&mut self, search: &mut ActiveSearch, offset: usize, wrap: bool) { if search.buffer_generation != self.buffer.generation() { unsafe { search.regex.set_text(&mut search.text, offset) }; search.buffer_generation = self.buffer.generation(); search.next_search_offset = offset; } else if search.next_search_offset != offset { search.next_search_offset = offset; search.regex.reset(offset); } let mut hit = search.regex.next(); // If we hit the end of the buffer, and we know that there's something to find, // start the search again from the beginning (= wrap around). if wrap && hit.is_none() && search.next_search_offset != 0 { search.next_search_offset = 0; search.regex.reset(0); hit = search.regex.next(); } search.selection_generation = if let Some(range) = hit { // Now the search offset is no more at the start of the buffer. search.next_search_offset = range.end; let beg = self.cursor_move_to_offset_internal(self.cursor, range.start); let end = self.cursor_move_to_offset_internal(beg, range.end); unsafe { self.set_cursor(end) }; self.make_cursor_visible(); self.set_selection(Some(TextBufferSelection { beg: beg.logical_pos, end: end.logical_pos, })) } else { // Avoid searching through the entire document again if we know there's nothing to find. search.no_matches = true; self.set_selection(None) }; } fn find_parse_replacement<'a>( arena: &'a Arena, search: &mut ActiveSearch, replacement: &[u8], ) -> BVec<'a, RegexReplacement<'a>> { let mut res = BVec::empty(); if !search.options.use_regex { return res; } let group_count = search.regex.group_count(); let mut text = BVec::empty(); let mut text_beg = 0; loop { let mut off = memchr2(b'$', b'\\', replacement, text_beg); // Push the raw, unescaped text, if any. if text_beg < off { text.extend_from_slice(arena, &replacement[text_beg..off]); } // Unescape any escaped characters. while off < replacement.len() && replacement[off] == b'\\' { off += 2; // If this backslash is the last character (e.g. because // `replacement` is just 1 byte long, holding just b"\\"), // we can't unescape it. In that case, we map it to `b'\\'` here. // This results in us appending a literal backslash to the text. let ch = replacement.get(off - 1).map_or(b'\\', |&c| c); // Unescape and append the character. text.push( arena, match ch { b'n' => b'\n', b'r' => b'\r', b't' => b'\t', ch => ch, }, ); } // Parse out a group number, if any. let mut group = -1; if off < replacement.len() && replacement[off] == b'$' { let mut beg = off; let mut end = off + 1; let mut acc = 0i32; let mut acc_bad = true; if end < replacement.len() { let ch = replacement[end]; if ch == b'$' { // Translate "$$" to "$". beg += 1; end += 1; } else if ch.is_ascii_digit() { // Parse "$1234" into 1234i32. // If the number is larger than the group count, // we flag `acc_bad` which causes us to treat it as text. acc_bad = false; while { acc = acc.wrapping_mul(10).wrapping_add((replacement[end] - b'0') as i32); acc_bad |= acc > group_count; end += 1; end < replacement.len() && replacement[end].is_ascii_digit() } {} } } if !acc_bad { group = acc; } else { text.extend_from_slice(arena, &replacement[beg..end]); } off = end; } if !text.is_empty() { res.push(arena, RegexReplacement::Text(text)); text = BVec::empty(); } if group >= 0 { res.push(arena, RegexReplacement::Group(group)); } text_beg = off; if text_beg >= replacement.len() { break; } } res } fn find_fill_replacement<'a>( &self, search: &mut ActiveSearch, replacement: &'a [u8], parsed_replacements: &[RegexReplacement], ) -> Cow<'a, [u8]> { if !search.options.use_regex { Cow::Borrowed(replacement) } else { let mut res = Vec::new(); for replacement in parsed_replacements { match replacement { RegexReplacement::Text(text) => res.extend_from_slice(text), RegexReplacement::Group(group) => { if let Some(range) = search.regex.group(*group) { self.buffer.extract_raw(range, &mut res, usize::MAX); } } } } Cow::Owned(res) } } fn measurement_config(&self) -> MeasurementConfig<'_> { MeasurementConfig::new(&self.buffer) .with_word_wrap_column(self.word_wrap_column) .with_tab_size(self.tab_size) } fn goto_line_start(&self, cursor: Cursor, y: CoordType) -> Cursor { let mut result = cursor; let mut seek_to_line_start = true; if y > result.logical_pos.y { while y > result.logical_pos.y { let chunk = self.read_forward(result.offset); if chunk.is_empty() { break; } let (delta, line) = simd::lines_fwd(chunk, 0, result.logical_pos.y, y); result.offset += delta; result.logical_pos.y = line; } // If we're at the end of the buffer, we could either be there because the last // character in the buffer is genuinely a newline, or because the buffer ends in a // line of text without trailing newline. The only way to make sure is to seek // backwards to the line start again. But otherwise we can skip that. seek_to_line_start = result.offset == self.text_length() && result.offset != cursor.offset; } if seek_to_line_start { loop { let chunk = self.read_backward(result.offset); if chunk.is_empty() { break; } let (delta, line) = simd::lines_bwd(chunk, chunk.len(), result.logical_pos.y, y); result.offset -= chunk.len() - delta; result.logical_pos.y = line; if delta > 0 { break; } } } if result.offset == cursor.offset { return result; } result.logical_pos.x = 0; result.visual_pos.x = 0; result.visual_pos.y = result.logical_pos.y; result.column = 0; result.wrap_opp = false; if self.word_wrap_column > 0 { let upward = result.offset < cursor.offset; let (top, bottom) = if upward { (result, cursor) } else { (cursor, result) }; let mut bottom_remeasured = self.measurement_config().with_cursor(top).goto_logical(bottom.logical_pos); // The second problem is that visual positions can be ambiguous. A single logical position // can map to two visual positions: One at the end of the preceding line in front of // a word wrap, and another at the start of the next line after the same word wrap. // // This, however, only applies if we go upwards, because only then `bottom ≅ cursor`, // and thus only then this `bottom` is ambiguous. Otherwise, `bottom ≅ result` // and `result` is at a line start which is never ambiguous. if upward { let a = bottom_remeasured.visual_pos.x; let b = bottom.visual_pos.x; bottom_remeasured.visual_pos.y = bottom_remeasured.visual_pos.y + (a != 0 && b == 0) as CoordType - (a == 0 && b != 0) as CoordType; } let mut delta = bottom_remeasured.visual_pos.y - top.visual_pos.y; if upward { delta = -delta; } result.visual_pos.y = cursor.visual_pos.y + delta; } result } fn cursor_move_to_offset_internal(&self, mut cursor: Cursor, offset: usize) -> Cursor { if offset == cursor.offset { return cursor; } // goto_line_start() is fast for seeking across lines _if_ line wrapping is disabled. // For backward seeking we have to use it either way, so we're covered there. // This implements the forward seeking portion, if it's approx. worth doing so. if self.word_wrap_column <= 0 && offset.saturating_sub(cursor.offset) > 1024 { // Replacing this with a more optimal, direct memchr() loop appears // to improve performance only marginally by another 2% or so. // Still, it's kind of "meh" looking at how poorly this is implemented... loop { let next = self.goto_line_start(cursor, cursor.logical_pos.y + 1); // Stop when we either ran past the target offset, // or when we hit the end of the buffer and `goto_line_start` backtracked to the line start. if next.offset > offset || next.offset <= cursor.offset { break; } cursor = next; } } while offset < cursor.offset { cursor = self.goto_line_start(cursor, cursor.logical_pos.y - 1); } self.measurement_config().with_cursor(cursor).goto_offset(offset) } fn cursor_move_to_logical_internal(&self, mut cursor: Cursor, pos: Point) -> Cursor { let pos = Point { x: pos.x.max(0), y: pos.y.max(0) }; if pos == cursor.logical_pos { return cursor; } // goto_line_start() is the fastest way for seeking across lines. As such we always // use it if the requested `.y` position is different. We still need to use it if the // `.x` position is smaller, but only because `goto_logical()` cannot seek backwards. if pos.y != cursor.logical_pos.y || pos.x < cursor.logical_pos.x { cursor = self.goto_line_start(cursor, pos.y); } self.measurement_config().with_cursor(cursor).goto_logical(pos) } fn cursor_move_to_visual_internal(&self, mut cursor: Cursor, pos: Point) -> Cursor { let pos = Point { x: pos.x.max(0), y: pos.y.max(0) }; if pos == cursor.visual_pos { return cursor; } if self.word_wrap_column <= 0 { // Identical to the fast-pass in `cursor_move_to_logical_internal()`. if pos.y != cursor.visual_pos.y || pos.x < cursor.visual_pos.x { cursor = self.goto_line_start(cursor, pos.y); } } else { // `goto_visual()` can only seek forward, so we need to seek backward here if needed. // NOTE that this intentionally doesn't use the `Eq` trait of `Point`, because if // `pos.y == cursor.visual_pos.y` we don't need to go to `cursor.logical_pos.y - 1`. while pos.y < cursor.visual_pos.y { cursor = self.goto_line_start(cursor, cursor.logical_pos.y - 1); } if pos.y == cursor.visual_pos.y && pos.x < cursor.visual_pos.x { cursor = self.goto_line_start(cursor, cursor.logical_pos.y); } } self.measurement_config().with_cursor(cursor).goto_visual(pos) } fn cursor_move_delta_internal( &self, mut cursor: Cursor, granularity: CursorMovement, mut delta: CoordType, ) -> Cursor { if delta == 0 { return cursor; } let sign = if delta > 0 { 1 } else { -1 }; match granularity { CursorMovement::Grapheme => { let start_x = if delta > 0 { 0 } else { CoordType::MAX }; loop { let target_x = cursor.logical_pos.x + delta; cursor = self.cursor_move_to_logical_internal( cursor, Point { x: target_x, y: cursor.logical_pos.y }, ); // We can stop if we ran out of remaining delta // (or perhaps ran past the goal; in either case the sign would've changed), // or if we hit the beginning or end of the buffer. delta = target_x - cursor.logical_pos.x; if delta.signum() != sign || (delta < 0 && cursor.offset == 0) || (delta > 0 && cursor.offset >= self.text_length()) { break; } cursor = self.cursor_move_to_logical_internal( cursor, Point { x: start_x, y: cursor.logical_pos.y + sign }, ); // We crossed a newline which counts for 1 grapheme cluster. // So, we also need to run the same check again. delta -= sign; if delta.signum() != sign || cursor.offset == 0 || cursor.offset >= self.text_length() { break; } } } CursorMovement::Word => { let doc = &self.buffer as &dyn ReadableDocument; let mut offset = self.cursor.offset; while delta != 0 { if delta < 0 { offset = navigation::word_backward(doc, offset); } else { offset = navigation::word_forward(doc, offset); } delta -= sign; } cursor = self.cursor_move_to_offset_internal(cursor, offset); } } cursor } /// Moves the cursor to the given offset. pub fn cursor_move_to_offset(&mut self, offset: usize) { unsafe { self.set_cursor(self.cursor_move_to_offset_internal(self.cursor, offset)) } } /// Moves the cursor to the given logical position. pub fn cursor_move_to_logical(&mut self, pos: Point) { unsafe { self.set_cursor(self.cursor_move_to_logical_internal(self.cursor, pos)) } } /// Moves the cursor to the given visual position. pub fn cursor_move_to_visual(&mut self, pos: Point) { unsafe { self.set_cursor(self.cursor_move_to_visual_internal(self.cursor, pos)) } } /// Moves the cursor by the given delta. pub fn cursor_move_delta(&mut self, granularity: CursorMovement, delta: CoordType) { unsafe { self.set_cursor(self.cursor_move_delta_internal(self.cursor, granularity, delta)) } } /// Sets the cursor to the given position, and clears the selection. /// /// # Safety /// /// This function performs no checks that the cursor is valid. "Valid" in this case means /// that the TextBuffer has not been modified since you received the cursor from this class. pub unsafe fn set_cursor(&mut self, cursor: Cursor) { self.set_cursor_internal(cursor); self.last_history_type = HistoryType::Other; self.set_selection(None); } fn set_cursor_for_selection(&mut self, cursor: Cursor) { let beg = match self.selection { Some(TextBufferSelection { beg, .. }) => beg, None => self.cursor.logical_pos, }; self.set_cursor_internal(cursor); self.last_history_type = HistoryType::Other; let end = self.cursor.logical_pos; self.set_selection(if beg == end { None } else { Some(TextBufferSelection { beg, end }) }); } fn set_cursor_internal(&mut self, cursor: Cursor) { debug_assert!(cursor.offset <= self.text_length()); debug_assert!(cursor.logical_pos.x >= 0); debug_assert!(cursor.logical_pos.y >= 0); debug_assert!(cursor.logical_pos.y <= self.stats.logical_lines); debug_assert!(cursor.visual_pos.x >= 0); debug_assert!(self.word_wrap_column <= 0 || cursor.visual_pos.x <= self.word_wrap_column); debug_assert!(cursor.visual_pos.y >= 0); debug_assert!(cursor.visual_pos.y <= self.stats.visual_lines); self.cursor = cursor; } /// Extracts a rectangular region of the text buffer and writes it to the framebuffer. /// The `destination` rect is framebuffer coordinates. The extracted region within this /// text buffer has the given `origin` and the same size as the `destination` rect. pub fn render( &mut self, origin: Point, destination: Rect, focused: bool, fb: &mut Framebuffer, ) -> Option { if destination.is_empty() { return None; } let width = destination.width(); let height = destination.height(); let line_number_width = self.margin_width.max(3) as usize - 3; let text_width = width - self.margin_width; let mut visualizer_buf = [0xE2, 0x90, 0x80]; // U+2400 in UTF8 let mut visual_pos_x_max = 0; // Pick the cursor closer to the `origin.y`. let mut cursor = { let a = self.cursor; let b = self.cursor_for_rendering.unwrap_or_default(); let da = (a.visual_pos.y - origin.y).abs(); let db = (b.visual_pos.y - origin.y).abs(); if da < db { a } else { b } }; let [selection_beg, selection_end] = match self.selection { None => [Point::MIN, Point::MIN], Some(TextBufferSelection { beg, end }) => minmax(beg, end), }; for y in 0..height { let scratch = scratch_arena(None); let mut line = BString::empty(); line.reserve(&*scratch, width as usize * 2); let visual_line = origin.y + y; let mut cursor_beg = self.cursor_move_to_visual_internal(cursor, Point { x: origin.x, y: visual_line }); let cursor_end = self.cursor_move_to_visual_internal( cursor_beg, Point { x: origin.x + text_width, y: visual_line }, ); // Accelerate the next render pass by remembering where we started off. if y == 0 { self.cursor_for_rendering = Some(cursor_beg); } if line_number_width != 0 { if visual_line >= self.stats.visual_lines { // Past the end of the buffer? Place " | " in the margin. // Since we know that we won't see line numbers greater than i64::MAX (9223372036854775807) // any time soon, we can use a static string as the template (`MARGIN`) and slice it, // because `line_number_width` can't possibly be larger than 19. let off = 19 - line_number_width; unsafe { std::hint::assert_unchecked(off < MARGIN_TEMPLATE.len()) }; line.push_str(&*scratch, &MARGIN_TEMPLATE[off..]); } else if self.word_wrap_column <= 0 || cursor_beg.logical_pos.x == 0 { // Regular line? Place "123 | " in the margin. arena_write_fmt!( &*scratch, line, "{:1$} │ ", cursor_beg.logical_pos.y + 1, line_number_width ); } else { // Wrapped line? Place " ... | " in the margin. let number_width = (cursor_beg.logical_pos.y + 1).ilog10() as usize + 1; arena_write_fmt!( &*scratch, line, "{0:1$}{0:∙<2$} │ ", "", line_number_width - number_width, number_width ); // Blending in the background color will "dim" the indicator dots. let left = destination.left; let top = destination.top + y; fb.blend_fg( Rect { left, top, right: left + line_number_width as CoordType, bottom: top + 1, }, fb.indexed_alpha(IndexedColor::Background, 1, 2), ); } } let mut selection_off = 0..0; // Figure out the selection range on this line, if any. if cursor_beg.visual_pos.y == visual_line && selection_beg <= cursor_end.logical_pos && selection_end >= cursor_beg.logical_pos { let mut cursor = cursor_beg; // By default, we assume the entire line is selected. let mut selection_pos_beg = 0; let mut selection_pos_end = COORD_TYPE_SAFE_MAX; selection_off.start = cursor_beg.offset; selection_off.end = cursor_end.offset; // The start of the selection is within this line. We need to update selection_beg. if selection_beg <= cursor_end.logical_pos && selection_beg >= cursor_beg.logical_pos { cursor = self.cursor_move_to_logical_internal(cursor, selection_beg); selection_off.start = cursor.offset; selection_pos_beg = cursor.visual_pos.x; } // The end of the selection is within this line. We need to update selection_end. if selection_end <= cursor_end.logical_pos && selection_end >= cursor_beg.logical_pos { cursor = self.cursor_move_to_logical_internal(cursor, selection_end); selection_off.end = cursor.offset; selection_pos_end = cursor.visual_pos.x; } let left = destination.left + self.margin_width - origin.x; let top = destination.top + y; let rect = Rect { left: left + selection_pos_beg.max(origin.x), top, right: left + selection_pos_end.min(origin.x + text_width), bottom: top + 1, }; let mut bg = fb.indexed(IndexedColor::Foreground).oklab_blend(fb.indexed_alpha( IndexedColor::BrightBlue, 1, 2, )); if !focused { bg = bg.oklab_blend(fb.indexed_alpha(IndexedColor::Background, 1, 2)); }; let fg = fb.contrasted(bg); fb.blend_bg(rect, bg); fb.blend_fg(rect, fg); } // Nothing to do if the entire line is empty. if cursor_beg.offset != cursor_end.offset { // If we couldn't reach the left edge, we may have stopped short due to a wide glyph. // In that case we'll try to find the next character and then compute by how many // columns it overlaps the left edge (can be anything between 1 and 7). if cursor_beg.visual_pos.x < origin.x { let cursor_next = self.cursor_move_to_logical_internal( cursor_beg, Point { x: cursor_beg.logical_pos.x + 1, y: cursor_beg.logical_pos.y }, ); if cursor_next.visual_pos.x > origin.x { let overlap = cursor_next.visual_pos.x - origin.x; debug_assert!((1..=7).contains(&overlap)); line.push_str(&*scratch, &TAB_WHITESPACE[..overlap as usize]); cursor_beg = cursor_next; } } let mut global_off = cursor_beg.offset; let mut cursor_line = cursor_beg; while global_off < cursor_end.offset { let chunk = self.read_forward(global_off); let chunk = &chunk[..chunk.len().min(cursor_end.offset - global_off)]; let mut it = Utf8Chars::new(chunk, 0); // TODO: Looping char-by-char is bad for performance. // >25% of the total rendering time is spent here. loop { let chunk_off = it.offset(); let global_off = global_off + chunk_off; let Some(ch) = it.next() else { break; }; if ch == ' ' || ch == '\t' { let is_tab = ch == '\t'; let visualize = selection_off.contains(&global_off); let mut whitespace = TAB_WHITESPACE; let mut prefix_add = 0; if is_tab || visualize { // We need the character's visual position in order to either compute the tab size, // or set the foreground color of the visualizer, respectively. // TODO: Doing this char-by-char is of course also bad for performance. cursor_line = self.cursor_move_to_offset_internal(cursor_line, global_off); } let tab_size = if is_tab { self.tab_size_eval(cursor_line.column) } else { 1 }; if visualize { // If the whitespace is part of the selection, // we replace " " with "・" and "\t" with "→". (whitespace, prefix_add) = if is_tab { (VISUAL_TAB, VISUAL_TAB_PREFIX_ADD) } else { (VISUAL_SPACE, VISUAL_SPACE_PREFIX_ADD) }; // Make the visualized characters slightly gray. let visualizer_rect = { let left = destination.left + self.margin_width + cursor_line.visual_pos.x - origin.x; let top = destination.top + cursor_line.visual_pos.y - origin.y; Rect { left, top, right: left + 1, bottom: top + 1 } }; fb.blend_fg( visualizer_rect, fb.indexed_alpha(IndexedColor::Foreground, 1, 2), ); } line.push_str(&*scratch, &whitespace[..prefix_add + tab_size as usize]); } else if ch <= '\x1f' || ('\u{7f}'..='\u{9f}').contains(&ch) { // Append a Unicode representation of the C0 or C1 control character. visualizer_buf[2] = if ch <= '\x1f' { 0x80 | ch as u8 // U+2400..=U+241F } else if ch == '\x7f' { 0xA1 // U+2421 } else { 0xA6 // U+2426, because there are no pictures for C1 control characters. }; // Our manually constructed UTF8 is never going to be invalid. Trust. line.push_str(&*scratch, unsafe { str::from_utf8_unchecked(&visualizer_buf) }); // Highlight the control character yellow. cursor_line = self.cursor_move_to_offset_internal(cursor_line, global_off); let visualizer_rect = { let left = destination.left + self.margin_width + cursor_line.visual_pos.x - origin.x; let top = destination.top + cursor_line.visual_pos.y - origin.y; Rect { left, top, right: left + 1, bottom: top + 1 } }; let bg = fb.indexed(IndexedColor::Yellow); let fg = fb.contrasted(bg); fb.blend_bg(visualizer_rect, bg); fb.blend_fg(visualizer_rect, fg); } else { line.push(&*scratch, ch); } } global_off += chunk.len(); } visual_pos_x_max = visual_pos_x_max.max(cursor_end.visual_pos.x); } fb.replace_text(destination.top + y, destination.left, destination.right, &line); cursor = cursor_end; } // Colorize the margin that we wrote above. if self.margin_width > 0 { let margin = Rect { left: destination.left, top: destination.top, right: destination.left + self.margin_width, bottom: destination.bottom, }; fb.blend_fg(margin, StraightRgba::from_le(0x7f7f7f7f)); } if self.ruler > 0 { let left = destination.left + self.margin_width + (self.ruler - origin.x).max(0); let right = destination.right; if left < right { fb.blend_bg( Rect { left, top: destination.top, right, bottom: destination.bottom }, fb.indexed_alpha(IndexedColor::BrightRed, 1, 4), ); } } if focused { let mut x = self.cursor.visual_pos.x; let mut y = self.cursor.visual_pos.y; if self.word_wrap_column > 0 && x >= self.word_wrap_column { // The line the cursor is on wraps exactly on the word wrap column which // means the cursor is invisible. We need to move it to the next line. x = 0; y += 1; } // Move the cursor into screen space. x += destination.left - origin.x + self.margin_width; y += destination.top - origin.y; let cursor = Point { x, y }; let text = Rect { left: destination.left + self.margin_width, top: destination.top, right: destination.right, bottom: destination.bottom, }; if text.contains(cursor) { fb.set_cursor(cursor, self.overtype); if self.line_highlight_enabled && selection_beg >= selection_end { fb.blend_bg( Rect { left: destination.left, top: cursor.y, right: destination.right, bottom: cursor.y + 1, }, StraightRgba::from_le(0x7f7f7f7f), ); } } } Some(RenderResult { visual_pos_x_max }) } pub fn cut(&mut self, clipboard: &mut Clipboard) { self.cut_copy(clipboard, true); } pub fn copy(&mut self, clipboard: &mut Clipboard) { self.cut_copy(clipboard, false); } fn cut_copy(&mut self, clipboard: &mut Clipboard, cut: bool) { let line_copy = !self.has_selection(); let selection = self.extract_selection(cut); clipboard.write(selection); clipboard.write_was_line_copy(line_copy); } pub fn paste(&mut self, clipboard: &Clipboard) { let data = clipboard.read(); if data.is_empty() { return; } let pos = self.cursor_logical_pos(); let at = if clipboard.is_line_copy() { self.goto_line_start(self.cursor, pos.y) } else { self.cursor }; self.write(data, at, true); if clipboard.is_line_copy() { self.cursor_move_to_logical(Point { x: pos.x, y: pos.y + 1 }); } } /// Inserts the user input `text` at the current cursor position. /// Replaces tabs with whitespace if needed, etc. pub fn write_canon(&mut self, text: &[u8]) { self.write(text, self.cursor, false); } /// Inserts `text` as-is at the current cursor position. /// The only transformation applied is that newlines are normalized. pub fn write_raw(&mut self, text: &[u8]) { self.write(text, self.cursor, true); } fn write(&mut self, text: &[u8], at: Cursor, raw: bool) { let history_type = if raw { HistoryType::Other } else { HistoryType::Write }; let mut edit_begun = false; // If we have an active selection, writing an empty `text` // will still delete the selection. As such, we check this first. if let Some((beg, end)) = self.selection_range_internal(false) { self.edit_begin(history_type, beg); self.edit_delete(end); self.set_selection(None); edit_begun = true; } // If the text is empty the remaining code won't do anything, // allowing us to exit early. if text.is_empty() { // ...we still need to end any active edit session though. if edit_begun { self.edit_end(); } return; } if !edit_begun { self.edit_begin(history_type, at); } let mut offset = 0; let scratch = scratch_arena(None); let mut newline_buffer = BString::empty(); loop { // Can't use `unicode::newlines_forward` because bracketed paste uses CR instead of LF/CRLF. let offset_next = memchr2(b'\r', b'\n', text, offset); let line = &text[offset..offset_next]; let column_before = self.cursor.logical_pos.x; // Write the contents of the line into the buffer. let mut line_off = 0; while line_off < line.len() { // Split the line into chunks of non-tabs and tabs. let mut plain = line; if !raw && !self.indent_with_tabs { let end = memchr2(b'\t', b'\t', line, line_off); plain = &line[line_off..end]; } // Non-tabs are written as-is, because the outer loop already handles newline translation. self.edit_write(plain); line_off += plain.len(); // Now replace tabs with spaces. while line_off < line.len() && line[line_off] == b'\t' { let spaces = self.tab_size_eval(self.cursor.column); let spaces = &TAB_WHITESPACE.as_bytes()[..spaces as usize]; self.edit_write(spaces); line_off += 1; } } if !raw && self.overtype { let delete = self.cursor.logical_pos.x - column_before; let end = self.cursor_move_to_logical_internal( self.cursor, Point { x: self.cursor.logical_pos.x + delete, y: self.cursor.logical_pos.y }, ); self.edit_delete(end); } offset += line.len(); if offset >= text.len() { break; } // First, write the newline. newline_buffer.clear(); newline_buffer.push_str(&*scratch, if self.newlines_are_crlf { "\r\n" } else { "\n" }); if !raw { // We'll give the next line the same indentation as the previous one. // This block figures out how much that is. We can't reuse that value, // because " a\n a\n" should give the 3rd line a total indentation of 4. // Assuming your terminal has bracketed paste, this won't be a concern though. // (If it doesn't, use a different terminal.) let line_beg = self.goto_line_start(self.cursor, self.cursor.logical_pos.y); let limit = self.cursor.offset; let mut off = line_beg.offset; let mut newline_indentation = 0; 'outer: while off < limit { let chunk = self.read_forward(off); let chunk = &chunk[..chunk.len().min(limit - off)]; for &c in chunk { if c == b' ' { newline_indentation += 1; } else if c == b'\t' { newline_indentation += self.tab_size_eval(newline_indentation); } else { break 'outer; } } off += chunk.len(); } // If tabs are enabled, add as many tabs as we can. if self.indent_with_tabs { let tab_count = newline_indentation / self.tab_size; newline_buffer.push_repeat(&*scratch, '\t', tab_count as usize); newline_indentation -= tab_count * self.tab_size; } // If tabs are disabled, or if the indentation wasn't a multiple of the tab size, // add spaces to make up the difference. newline_buffer.push_repeat(&*scratch, ' ', newline_indentation as usize); } self.edit_write(newline_buffer.as_bytes()); // Skip one CR/LF/CRLF. if offset >= text.len() { break; } if text[offset] == b'\r' { offset += 1; } if offset >= text.len() { break; } if text[offset] == b'\n' { offset += 1; } if offset >= text.len() { break; } } // POSIX mandates that all valid lines end in a newline. // This isn't all that common on Windows and so we have // `self.final_newline` to control this. // // In order to not annoy people with this, we only add a // newline if you just edited the very end of the buffer. if self.insert_final_newline && self.cursor.offset > 0 && self.cursor.offset == self.text_length() && self.cursor.logical_pos.x > 0 { let cursor = self.cursor; self.edit_write(if self.newlines_are_crlf { b"\r\n" } else { b"\n" }); // Can't use `set_cursor_internal` here, because we haven't updated the line stats yet. self.cursor = cursor; } self.edit_end(); } /// Deletes 1 grapheme cluster from the buffer. /// `cursor_movements` is expected to be -1 for backspace and 1 for delete. /// If there's a current selection, it will be deleted and `cursor_movements` ignored. /// The selection is cleared after the call. /// Deletes characters from the buffer based on a delta from the cursor. pub fn delete(&mut self, granularity: CursorMovement, delta: CoordType) { if delta == 0 { return; } let mut beg; let mut end; if let Some(r) = self.selection_range_internal(false) { (beg, end) = r; } else { if (delta < 0 && self.cursor.offset == 0) || (delta > 0 && self.cursor.offset >= self.text_length()) { // Nothing to delete. return; } beg = self.cursor; end = self.cursor_move_delta_internal(beg, granularity, delta); if beg.offset == end.offset { return; } if beg.offset > end.offset { mem::swap(&mut beg, &mut end); } } self.edit_begin(HistoryType::Delete, beg); self.edit_delete(end); self.edit_end(); self.set_selection(None); } /// Returns the logical position of the first character on this line. /// Return `.x == 0` if there are no non-whitespace characters. pub fn indent_end_logical_pos(&self) -> Point { let cursor = self.goto_line_start(self.cursor, self.cursor.logical_pos.y); let (chars, _) = self.measure_indent_internal(cursor.offset, CoordType::MAX); Point { x: chars, y: cursor.logical_pos.y } } /// Indents/unindents the current selection or line. pub fn indent_change(&mut self, direction: CoordType) { let selection = self.selection; let mut selection_beg = self.cursor.logical_pos; let mut selection_end = selection_beg; if let Some(TextBufferSelection { beg, end }) = &selection { selection_beg = *beg; selection_end = *end; } if direction >= 0 && self.selection.is_none_or(|sel| sel.beg.y == sel.end.y) { self.write_canon(b"\t"); return; } self.edit_begin_grouping(); for y in selection_beg.y.min(selection_end.y)..=selection_beg.y.max(selection_end.y) { self.cursor_move_to_logical(Point { x: 0, y }); let line_start_offset = self.cursor.offset; let (curr_chars, curr_columns) = self.measure_indent_internal(line_start_offset, CoordType::MAX); self.cursor_move_to_logical(Point { x: curr_chars, y: self.cursor.logical_pos.y }); let delta; if direction < 0 { // Unindent the line. If there's no indentation, skip. if curr_columns <= 0 { continue; } let (prev_chars, _) = self.measure_indent_internal( line_start_offset, self.tab_size_prev_column(curr_columns), ); delta = prev_chars - curr_chars; self.delete(CursorMovement::Grapheme, delta); } else { // Indent the line. `self.cursor` is already at the level of indentation. delta = self.tab_size_eval(curr_columns); self.write_canon(b"\t"); } // As the lines get unindented, the selection should shift with them. if y == selection_beg.y { selection_beg.x += delta; } if y == selection_end.y { selection_end.x += delta; } } self.edit_end_grouping(); // Move the cursor to the new end of the selection. self.set_cursor_internal(self.cursor_move_to_logical_internal(self.cursor, selection_end)); // NOTE: If the selection was previously `None`, // it should continue to be `None` after this. self.set_selection( selection.map(|_| TextBufferSelection { beg: selection_beg, end: selection_end }), ); } fn measure_indent_internal( &self, mut offset: usize, max_columns: CoordType, ) -> (CoordType, CoordType) { let mut chars = 0; let mut columns = 0; 'outer: loop { let chunk = self.read_forward(offset); if chunk.is_empty() { break; } for &c in chunk { let next = match c { b' ' => columns + 1, b'\t' => columns + self.tab_size_eval(columns), _ => break 'outer, }; if next > max_columns { break 'outer; } chars += 1; columns = next; } offset += chunk.len(); // No need to do another round if we // already got the exact right amount. if columns >= max_columns { break; } } (chars, columns) } /// Displaces the current, cursor or the selection, line(s) in the given direction. pub fn move_selected_lines(&mut self, direction: MoveLineDirection) { let selection = self.selection; let cursor = self.cursor; // If there's no selection, we move the line the cursor is on instead. let [beg, end] = match self.selection { Some(s) => minmax(s.beg.y, s.end.y), None => [cursor.logical_pos.y, cursor.logical_pos.y], }; // Check if this would be a no-op. if match direction { MoveLineDirection::Up => beg <= 0, MoveLineDirection::Down => end >= self.stats.logical_lines - 1, } { return; } let delta = match direction { MoveLineDirection::Up => -1, MoveLineDirection::Down => 1, }; let (cut, paste) = match direction { MoveLineDirection::Up => (beg - 1, end), MoveLineDirection::Down => (end + 1, beg), }; self.edit_begin_grouping(); { // Let's say this is `MoveLineDirection::Up`. // In that case, we'll cut (remove) the line above the selection here... self.cursor_move_to_logical(Point { x: 0, y: cut }); let line = self.extract_selection(true); // ...and paste it below the selection. This will then // appear to the user as if the selection was moved up. self.cursor_move_to_logical(Point { x: 0, y: paste }); self.edit_begin(HistoryType::Write, self.cursor); // The `extract_selection` call can return an empty `Vec`), // if the `cut` line was at the end of the file. Since we want to // paste the line somewhere it needs a trailing newline at the minimum. // // Similarly, if the `paste` line is at the end of the file // and there's no trailing newline, we'll have failed to reach // that end in which case `logical_pos.y != past`. if line.is_empty() || self.cursor.logical_pos.y != paste { self.write_canon(b"\n"); } if !line.is_empty() { self.write_raw(&line); } self.edit_end(); } self.edit_end_grouping(); // Shift the cursor and selection together with the moved lines. self.cursor_move_to_logical(Point { x: cursor.logical_pos.x, y: cursor.logical_pos.y + delta, }); self.set_selection(selection.map(|mut s| { s.beg.y += delta; s.end.y += delta; s })); } /// Extracts the contents of the current selection. /// May optionally delete it, if requested. This is meant to be used for Ctrl+X. fn extract_selection(&mut self, delete: bool) -> Vec { let line_copy = !self.has_selection(); let Some((beg, end)) = self.selection_range_internal(true) else { return Vec::new(); }; let mut out = Vec::new(); self.buffer.extract_raw(beg.offset..end.offset, &mut out, 0); if delete && !out.is_empty() { self.edit_begin(HistoryType::Delete, beg); self.edit_delete(end); self.edit_end(); self.set_selection(None); } // Line copies (= Ctrl+C when there's no selection) always end with a newline. if line_copy && !out.ends_with(b"\n") { out.replace_range(out.len().., if self.newlines_are_crlf { b"\r\n" } else { b"\n" }); } out } /// Extracts the contents of the current selection the user made. /// This differs from [`TextBuffer::extract_selection()`] in that /// it does nothing if the selection was made by searching. pub fn extract_user_selection(&mut self, delete: bool) -> Option> { if !self.has_selection() { return None; } if let Some(search) = &self.search { let search = unsafe { &*search.get() }; if search.selection_generation == self.selection_generation { return None; } } Some(self.extract_selection(delete)) } /// Returns the current selection anchors, or `None` if there /// is no selection. The returned logical positions are sorted. pub fn selection_range(&self) -> Option<(Cursor, Cursor)> { self.selection_range_internal(false) } /// Returns the current selection anchors. /// /// If there's no selection and `line_fallback` is `true`, /// the start/end of the current line are returned. /// This is meant to be used for Ctrl+C / Ctrl+X. fn selection_range_internal(&self, line_fallback: bool) -> Option<(Cursor, Cursor)> { let [beg, end] = match self.selection { None if !line_fallback => return None, None => [ Point { x: 0, y: self.cursor.logical_pos.y }, Point { x: 0, y: self.cursor.logical_pos.y + 1 }, ], Some(TextBufferSelection { beg, end }) => minmax(beg, end), }; let beg = self.cursor_move_to_logical_internal(self.cursor, beg); let end = self.cursor_move_to_logical_internal(beg, end); if beg.offset < end.offset { Some((beg, end)) } else { None } } fn edit_begin_grouping(&mut self) { self.active_edit_group = Some(ActiveEditGroupInfo { cursor_before: self.cursor.logical_pos, selection_before: self.selection, stats_before: self.stats, generation_before: self.buffer.generation(), }); } fn edit_end_grouping(&mut self) { self.active_edit_group = None; } /// Starts a new edit operation. /// This is used for tracking the undo/redo history. fn edit_begin(&mut self, history_type: HistoryType, cursor: Cursor) { self.active_edit_depth += 1; if self.active_edit_depth > 1 { return; } let cursor_before = self.cursor; self.set_cursor_internal(cursor); // If both the last and this are a Write/Delete operation, we skip allocating a new undo history item. if history_type != self.last_history_type || !matches!(history_type, HistoryType::Write | HistoryType::Delete) { self.redo_stack.clear(); while self.undo_stack.len() > 1000 { self.undo_stack.pop_front(); } self.last_history_type = history_type; self.undo_stack.push_back(SemiRefCell::new(HistoryEntry { cursor_before: cursor_before.logical_pos, selection_before: self.selection, stats_before: self.stats, generation_before: self.buffer.generation(), cursor: cursor.logical_pos, deleted: Vec::new(), added: Vec::new(), })); if let Some(info) = &self.active_edit_group && let Some(entry) = self.undo_stack.back() { let mut entry = entry.borrow_mut(); entry.cursor_before = info.cursor_before; entry.selection_before = info.selection_before; entry.stats_before = info.stats_before; entry.generation_before = info.generation_before; } } self.active_edit_off = cursor.offset; // If word-wrap is enabled, the visual layout of all logical lines affected by the write // may have changed. This includes even text before the insertion point up to the line // start, because this write may have joined with a word before the initial cursor. // See other uses of `word_wrap_cursor_next_line` in this function. if self.word_wrap_column > 0 { let safe_start = self.goto_line_start(cursor, cursor.logical_pos.y); let next_line = self.cursor_move_to_logical_internal( cursor, Point { x: 0, y: cursor.logical_pos.y + 1 }, ); self.active_edit_line_info = Some(ActiveEditLineInfo { safe_start, line_height_in_rows: next_line.visual_pos.y - safe_start.visual_pos.y, distance_next_line_start: next_line.offset - cursor.offset, }); } } /// Writes `text` into the buffer at the current cursor position. /// It records the change in the undo stack. fn edit_write(&mut self, text: &[u8]) { let logical_y_before = self.cursor.logical_pos.y; // Copy the written portion into the undo entry. { let mut undo = self.undo_stack.back_mut().unwrap().borrow_mut(); undo.added.extend_from_slice(text); } // Write! self.buffer.replace(self.active_edit_off..self.active_edit_off, text); // Move self.cursor to the end of the newly written text. Can't use `self.set_cursor_internal`, // because we're still in the progress of recalculating the line stats. self.active_edit_off += text.len(); self.cursor = self.cursor_move_to_offset_internal(self.cursor, self.active_edit_off); self.stats.logical_lines += self.cursor.logical_pos.y - logical_y_before; } /// Deletes the text between the current cursor position and `to`. /// It records the change in the undo stack. fn edit_delete(&mut self, to: Cursor) { debug_assert!(to.offset >= self.active_edit_off); let logical_y_before = self.cursor.logical_pos.y; let off = self.active_edit_off; let mut out_off = usize::MAX; let mut undo = self.undo_stack.back_mut().unwrap().borrow_mut(); // If this is a continued backspace operation, // we need to prepend the deleted portion to the undo entry. if self.cursor.logical_pos < undo.cursor { out_off = 0; undo.cursor = self.cursor.logical_pos; } // Copy the deleted portion into the undo entry. let deleted = &mut undo.deleted; self.buffer.extract_raw(off..to.offset, deleted, out_off); // Delete the portion from the buffer by enlarging the gap. let count = to.offset - off; self.buffer.allocate_gap(off, 0, count); self.stats.logical_lines += logical_y_before - to.logical_pos.y; } /// Finalizes the current edit operation /// and recalculates the line statistics. fn edit_end(&mut self) { self.active_edit_depth -= 1; debug_assert!(self.active_edit_depth >= 0); if self.active_edit_depth > 0 { return; } #[cfg(debug_assertions)] { let entry = self.undo_stack.back_mut().unwrap().borrow_mut(); debug_assert!(!entry.deleted.is_empty() || !entry.added.is_empty()); } if let Some(info) = self.active_edit_line_info.take() { let deleted_count = self.undo_stack.back_mut().unwrap().borrow_mut().deleted.len(); let target = self.cursor.logical_pos; // From our safe position we can measure the actual visual position of the cursor. self.set_cursor_internal(self.cursor_move_to_logical_internal(info.safe_start, target)); // If content is added at the insertion position, that's not a problem: // We can just remeasure the height of this one line and calculate the delta. // `deleted_count` is 0 in this case. // // The problem is when content is deleted, because it may affect lines // beyond the end of the `next_line`. In that case we have to measure // the entire buffer contents until the end to compute `self.stats.visual_lines`. if deleted_count < info.distance_next_line_start { // Now we can measure how many more visual rows this logical line spans. let next_line = self .cursor_move_to_logical_internal(self.cursor, Point { x: 0, y: target.y + 1 }); let lines_before = info.line_height_in_rows; let lines_after = next_line.visual_pos.y - info.safe_start.visual_pos.y; self.stats.visual_lines += lines_after - lines_before; } else { let end = self.cursor_move_to_logical_internal(self.cursor, Point::MAX); self.stats.visual_lines = end.visual_pos.y + 1; } } else { // If word-wrap is disabled the visual line count always matches the logical one. self.stats.visual_lines = self.stats.logical_lines; } self.recalc_after_content_changed(); } /// Undo the last edit operation. pub fn undo(&mut self) { self.undo_redo(true); } /// Redo the last undo operation. pub fn redo(&mut self) { self.undo_redo(false); } fn undo_redo(&mut self, undo: bool) { let buffer_generation = self.buffer.generation(); let mut entry_buffer_generation = None; let mut damage_start = CoordType::MAX; loop { // Transfer the last entry from the undo stack to the redo stack or vice versa. { let (from, to) = if undo { (&mut self.undo_stack, &mut self.redo_stack) } else { (&mut self.redo_stack, &mut self.undo_stack) }; // Only pop the entry if its buffer generation matches the previous one let Some(g) = from.pop_back_if(|c| { entry_buffer_generation.is_none_or(|g| g == c.borrow().generation_before) }) else { break; }; to.push_back(g); } let change = { let to = if undo { &self.redo_stack } else { &self.undo_stack }; to.back().unwrap() }; // Remember the buffer generation of the change so we can stop popping undos/redos. // Also, move to the point where the modification took place. let cursor = { let change = change.borrow(); entry_buffer_generation = Some(change.generation_before); self.cursor_move_to_logical_internal(self.cursor, change.cursor) }; let safe_cursor = if self.word_wrap_column > 0 { // If word-wrap is enabled, we need to move the cursor to the beginning of the line. // This is because the undo/redo operation may have changed the visual position of the cursor. self.goto_line_start(cursor, cursor.logical_pos.y) } else { cursor }; damage_start = damage_start.min(cursor.logical_pos.y); { let mut change = change.borrow_mut(); let change = &mut *change; // Undo: Whatever was deleted is now added and vice versa. mem::swap(&mut change.deleted, &mut change.added); // Delete the inserted portion. self.buffer.allocate_gap(cursor.offset, 0, change.deleted.len()); // Reinsert the deleted portion. { let added = &change.added[..]; let mut beg = 0; let mut offset = cursor.offset; while beg < added.len() { let (end, line) = simd::lines_fwd(added, beg, 0, 1); let has_newline = line != 0; let link = &added[beg..end]; let line = unicode::strip_newline(link); let mut written; { let gap = self.buffer.allocate_gap(offset, line.len() + 2, 0); written = slice_copy_safe(gap, line); if has_newline { if self.newlines_are_crlf && written < gap.len() { gap[written] = b'\r'; written += 1; } if written < gap.len() { gap[written] = b'\n'; written += 1; } } self.buffer.commit_gap(written); } beg = end; offset += written; } } // Restore the previous line statistics. mem::swap(&mut self.stats, &mut change.stats_before); // Restore the previous selection. mem::swap(&mut self.selection, &mut change.selection_before); // Pretend as if the buffer was never modified. self.buffer.set_generation(change.generation_before); change.generation_before = buffer_generation; // Restore the previous cursor. let cursor_before = self.cursor_move_to_logical_internal(safe_cursor, change.cursor_before); change.cursor_before = self.cursor.logical_pos; // Can't use `set_cursor_internal` here, because we haven't updated the line stats yet. self.cursor = cursor_before; if self.undo_stack.is_empty() { self.last_history_type = HistoryType::Other; } } } if damage_start == CoordType::MAX { // There weren't any undo/redo entries. return; } if entry_buffer_generation.is_some() { self.recalc_after_content_changed(); } } /// For interfacing with ICU. pub(crate) fn read_backward(&self, off: usize) -> &[u8] { self.buffer.read_backward(off) } /// For interfacing with ICU. pub fn read_forward(&self, off: usize) -> &[u8] { self.buffer.read_forward(off) } } pub enum Bom { None, UTF8, UTF16LE, UTF16BE, UTF32LE, UTF32BE, GB18030, } const BOM_MAX_LEN: usize = 4; fn detect_bom(bytes: &[u8]) -> Option<&'static str> { if bytes.len() >= 4 { if bytes.starts_with(b"\xFF\xFE\x00\x00") { return Some("UTF-32LE"); } if bytes.starts_with(b"\x00\x00\xFE\xFF") { return Some("UTF-32BE"); } if bytes.starts_with(b"\x84\x31\x95\x33") { return Some("GB18030"); } } if bytes.len() >= 3 && bytes.starts_with(b"\xEF\xBB\xBF") { return Some("UTF-8"); } if bytes.len() >= 2 { if bytes.starts_with(b"\xFF\xFE") { return Some("UTF-16LE"); } if bytes.starts_with(b"\xFE\xFF") { return Some("UTF-16BE"); } } None } ================================================ FILE: crates/edit/src/buffer/navigation.rs ================================================ // 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(separators: &[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 = separators.len(); while i < len { let ch = separators[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(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 separator 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 { 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); } } ================================================ FILE: crates/edit/src/cell.rs ================================================ // 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 = std::cell::RefCell; 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(std::cell::UnsafeCell); impl SemiRefCell { #[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: &Self) -> Self { 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 } } } ================================================ FILE: crates/edit/src/clipboard.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! Clipboard facilities for the editor. /// The builtin, internal clipboard of the editor. /// /// This is useful particularly when the terminal doesn't support /// OSC 52 or when the clipboard contents are huge (e.g. 1GiB). #[derive(Default)] pub struct Clipboard { data: Vec, line_copy: bool, wants_host_sync: bool, } impl Clipboard { /// If true, we should emit a OSC 52 sequence to sync the clipboard /// with the hosting terminal. pub fn wants_host_sync(&self) -> bool { self.wants_host_sync } /// Call this once the clipboard has been synchronized with the host. pub fn mark_as_synchronized(&mut self) { self.wants_host_sync = false; } /// The editor has a special behavior when you have no selection and press /// Ctrl+C: It copies the current line to the clipboard. Then, when you /// paste it, it inserts the line at *the start* of the current line. /// This effectively prepends the current line with the copied line. /// `clipboard_line_start` is true in that case. pub fn is_line_copy(&self) -> bool { self.line_copy } /// Returns the current contents of the clipboard. pub fn read(&self) -> &[u8] { &self.data } /// Fill the clipboard with the given data. pub fn write(&mut self, data: Vec) { if !data.is_empty() { self.data = data; self.line_copy = false; self.wants_host_sync = true; } } /// See [`Clipboard::is_line_copy`]. pub fn write_was_line_copy(&mut self, line_copy: bool) { self.line_copy = line_copy; } } ================================================ FILE: crates/edit/src/document.rs ================================================ // 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 stdext::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, 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, replacement: &[u8]) { // `replacement` is not guaranteed to be valid UTF-8, so we need to sanitize it. let utf8 = String::from_utf8_lossy(replacement); // SAFETY: `range` is guaranteed to be on codepoint boundaries. unsafe { self.as_mut_vec() }.replace_range(range, utf8.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, replacement: &[u8]) { let mut vec = mem::take(self).into_os_string().into_encoded_bytes(); vec.replace_range(range, replacement); *self = unsafe { Self::from(OsString::from_encoded_bytes_unchecked(vec)) }; } } ================================================ FILE: crates/edit/src/framebuffer.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! A shoddy framebuffer for terminal applications. use std::cell::Cell; use std::ops::{BitOr, BitXor}; use std::ptr; use std::slice::ChunksExact; use stdext::arena::Arena; use stdext::arena_write_fmt; use stdext::collections::BString; use stdext::simd::memset; use crate::helpers::{CoordType, Point, Rect, Size}; use crate::oklab::StraightRgba; 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, } impl> From for IndexedColor { fn from(value: T) -> Self { unsafe { std::mem::transmute(value.into() & 0xF) } } } /// Number of indices used by [`IndexedColor`]. pub const INDEXED_COLORS_COUNT: usize = 18; /// Fallback theme. Matches Windows Terminal's Ottosson theme. pub const DEFAULT_THEME: [StraightRgba; INDEXED_COLORS_COUNT] = [ StraightRgba::from_be(0x000000ff), // Black StraightRgba::from_be(0xbe2c21ff), // Red StraightRgba::from_be(0x3fae3aff), // Green StraightRgba::from_be(0xbe9a4aff), // Yellow StraightRgba::from_be(0x204dbeff), // Blue StraightRgba::from_be(0xbb54beff), // Magenta StraightRgba::from_be(0x00a7b2ff), // Cyan StraightRgba::from_be(0xbebebeff), // White StraightRgba::from_be(0x808080ff), // BrightBlack StraightRgba::from_be(0xff3e30ff), // BrightRed StraightRgba::from_be(0x58ea51ff), // BrightGreen StraightRgba::from_be(0xffc944ff), // BrightYellow StraightRgba::from_be(0x2f6affff), // BrightBlue StraightRgba::from_be(0xfc74ffff), // BrightMagenta StraightRgba::from_be(0x00e1f0ff), // BrightCyan StraightRgba::from_be(0xffffffff), // BrightWhite // -------- StraightRgba::from_be(0x000000ff), // Background StraightRgba::from_be(0xbebebeff), // Foreground ]; /// 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: [StraightRgba; INDEXED_COLORS_COUNT], /// Front and back buffers. Indexed by `frame_counter & 1`. buffers: [Buffer; 2], /// The current frame counter. Increments on every `flip` call. 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: [StraightRgba; 2], /// Above this lightness value, we consider a color to be "light". auto_color_threshold: f32, /// A cache table for previously contrasted colors. /// See: contrast_colors: [Cell<(StraightRgba, StraightRgba)>; CACHE_TABLE_SIZE], background_fill: StraightRgba, foreground_fill: StraightRgba, } 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::BrightWhite as usize], ], auto_color_threshold: 0.5, contrast_colors: [const { Cell::new((StraightRgba::zero(), StraightRgba::zero())) }; CACHE_TABLE_SIZE], background_fill: DEFAULT_THEME[IndexedColor::Background as usize], foreground_fill: DEFAULT_THEME[IndexedColor::Foreground as usize], } } /// 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: [StraightRgba; INDEXED_COLORS_COUNT]) { self.indexed_colors = colors; self.background_fill = StraightRgba::zero(); self.foreground_fill = StraightRgba::zero(); self.auto_colors = [ self.indexed_colors[IndexedColor::Black as usize], self.indexed_colors[IndexedColor::BrightWhite as usize], ]; // It's not guaranteed that Black is actually dark and BrightWhite light (vice versa for a light theme). // Such is the case with macOS 26's "Clear Dark" theme (and probably a lot other themes). // Its black is #35424C (l=0.3716; oof!) and bright white is #E5EFF5 (l=0.9464). // If we have a color such as #43698A (l=0.5065), which is l>0.5 ("light") and need a contrasting color, // we need that to be #E5EFF5, even though that's also l>0.5. With a midpoint of 0.659, we get that right. let lightness = self.auto_colors.map(|c| c.as_oklab().lightness()); self.auto_color_threshold = (lightness[0] + lightness[1]) * 0.5; // Ensure [0] is dark and [1] is light. if lightness[0] > lightness[1] { self.auto_colors.swap(0, 1); } } /// 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(StraightRgba::from_le(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) -> StraightRgba { 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, ) -> StraightRgba { let c = self.indexed_colors[index as usize].to_le(); let a = 255 * numerator / denominator; StraightRgba::from_le(a << 24 | (c & 0x00ffffff)) } /// Returns a color opposite to the brightness of the given `color`. pub fn contrasted(&self, color: StraightRgba) -> StraightRgba { let idx = (color.to_ne() as usize).wrapping_mul(HASH_MULTIPLIER) >> CACHE_TABLE_SHIFT; let slot = self.contrast_colors[idx].get(); if slot.0 == color { slot.1 } else { self.contrasted_slow(color) } } #[cold] fn contrasted_slow(&self, color: StraightRgba) -> StraightRgba { let idx = (color.to_ne() as usize).wrapping_mul(HASH_MULTIPLIER) >> CACHE_TABLE_SHIFT; let is_dark = color.as_oklab().lightness() < self.auto_color_threshold; let contrast = self.auto_colors[is_dark as usize]; self.contrast_colors[idx].set((color, contrast)); contrast } /// 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: StraightRgba) { 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: StraightRgba) { 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) -> BString<'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 = BString::empty(); 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(arena, "\x1b[m"); } arena_write_fmt!(arena, 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.to_ne() as u64 { last_bg = bg.to_ne() as u64; self.format_color(arena, &mut result, false, bg); } if last_fg != fg.to_ne() as u64 { last_fg = fg.to_ne() as u64; self.format_color(arena, &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(arena, "\x1b[3m"); } else { result.push_str(arena, "\x1b[23m"); } } if diff.is(Attributes::Underlined) { if attr.is(Attributes::Underlined) { result.push_str(arena, "\x1b[4m"); } else { result.push_str(arena, "\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(arena, &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. arena_write_fmt!( arena, 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(arena, "\x1b[?25l"); } } result } fn format_color<'a>( &self, arena: &'a Arena, dst: &mut BString<'a>, fg: bool, mut color: StraightRgba, ) { 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.to_ne() == 0 { arena_write_fmt!(arena, dst, "\x1b[{typ}9m"); return; } if color.alpha() != 0xff { let idx = if fg { IndexedColor::Foreground } else { IndexedColor::Background }; let dst = self.indexed(idx); color = dst.oklab_blend(color); } let r = color.red(); let g = color.green(); let b = color.blue(); arena_write_fmt!(arena, 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, 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 stopped 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, size: Size, } impl Bitmap { fn new(size: Size) -> Self { Self { data: vec![StraightRgba::zero(); (size.width * size.height) as usize], size } } fn fill(&mut self, color: StraightRgba) { 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: StraightRgba) { if color.alpha() == 0 { 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.alpha() == 0xff { 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 = c.oklab_blend(color); memset(&mut data[chunk_beg..chunk_end], c); off < end } {} } } } /// Iterates over each row in the bitmap. fn iter(&self) -> ChunksExact<'_, StraightRgba> { 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: Self = Self(0); pub const Italic: Self = Self(0b1); pub const Underlined: Self = Self(0b10); pub const All: Self = Self(0b11); pub const fn is(self, attr: Self) -> bool { (self.0 & attr.0) == attr.0 } } impl BitOr for Attributes { type Output = Self; fn bitor(self, rhs: Self) -> Self::Output { Self(self.0 | rhs.0) } } impl BitXor for Attributes { type Output = Self; fn bitxor(self, rhs: Self) -> Self::Output { Self(self.0 ^ rhs.0) } } /// Stores VT attributes for the framebuffer. #[derive(Default)] struct AttributeBuffer { data: Vec, 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 } } } ================================================ FILE: crates/edit/src/fuzzy.rs ================================================ // 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 stdext::arena::{Arena, scratch_arena}; use stdext::collections::BVec; 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, BVec<'a, usize>) { if haystack.is_empty() || needle.is_empty() { // return early if target or query are empty return (NO_MATCH, BVec::empty()); } 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, BVec::empty()); } 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 = BVec::empty(); let mut matches = BVec::empty(); scores.extend(&*scratch, std::iter::repeat_n(0, area)); matches.extend(&*scratch, std::iter::repeat_n(0, area)); // // 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 anymore, 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 above 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 = BVec::empty(); 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(arena, 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, 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 == '/' && b == '\\') || (a == '\\' && 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) -> BVec<'a, char> { let mut chars = BVec::empty(); chars.extend_sloppy(arena, s.chars()); chars } ================================================ FILE: crates/edit/src/glob.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! Simple glob matching. //! //! Supported patterns: //! - `*` matches any characters except for path separators, including an empty string. //! - `**` matches any characters, including an empty string. //! For convenience, `/**/` also matches `/`. use std::path::is_separator; #[inline] pub fn glob_match, N: AsRef<[u8]>>(pattern: P, name: N) -> bool { glob(pattern.as_ref(), name.as_ref()) } fn glob(pattern: &[u8], name: &[u8]) -> bool { fast_path(pattern, name).unwrap_or_else(|| slow_path(pattern, name)) } // Fast-pass for the most common patterns: // * Matching files by extension (e.g., **/*.rs) // * Matching files by name (e.g., **/Cargo.toml) fn fast_path(pattern: &[u8], name: &[u8]) -> Option { // In either case, the glob must start with "**/". let mut suffix = pattern.strip_prefix(b"**/")?; if suffix.is_empty() { return None; } // Determine whether it's "**/" or "**/*". let mut needs_dir_anchor = true; if let Some(s) = suffix.strip_prefix(b"*") { suffix = s; needs_dir_anchor = false; } // Restrict down to anything we can handle with a suffix check. if suffix.is_empty() || contains_magic(suffix) { return None; } Some( match_path_suffix(name, suffix) && ( // In case of "**/*extension" a simple suffix match is sufficient. !needs_dir_anchor // But for "**/filename" we need to ensure that path is either "filename"... || name.len() == suffix.len() // ...or that it is ".../filename". || is_separator(name[name.len() - suffix.len() - 1] as char) ), ) } fn contains_magic(pattern: &[u8]) -> bool { pattern.contains(&b'*') } fn match_path_suffix(path: &[u8], suffix: &[u8]) -> bool { if path.len() < suffix.len() { return false; } let path = &path[path.len() - suffix.len()..]; #[cfg(windows)] { path.iter().zip(suffix.iter()).all(|(a, b)| { let a = if *a == b'\\' { b'/' } else { *a }; let b = if *b == b'\\' { b'/' } else { *b }; a.eq_ignore_ascii_case(&b) }) } #[cfg(not(windows))] path.eq_ignore_ascii_case(suffix) } // This code is based on https://research.swtch.com/glob.go // It's not particularly fast, but it doesn't need to be. It doesn't run often. #[cold] fn slow_path(pattern: &[u8], name: &[u8]) -> bool { let mut px = 0; let mut nx = 0; let mut next_px = 0; let mut next_nx = 0; let mut is_double_star = false; while px < pattern.len() || nx < name.len() { if px < pattern.len() { match pattern[px] { b'*' => { // Try to match at nx. If that doesn't work out, restart at nx+1 next. next_px = px; next_nx = nx + 1; px += 1; is_double_star = false; if px < pattern.len() && pattern[px] == b'*' { px += 1; is_double_star = true; // For convenience, /**/ also matches / if px >= 3 && px < pattern.len() && pattern[px] == b'/' && pattern[px - 3] == b'/' { px += 1; } } continue; } c => { if nx < name.len() && name[nx].eq_ignore_ascii_case(&c) { px += 1; nx += 1; continue; } } } } // Mismatch. Maybe restart. if next_nx > 0 && next_nx <= name.len() && (is_double_star || !is_separator(name[next_nx - 1] as char)) { px = next_px; nx = next_nx; continue; } return false; } true } #[cfg(test)] mod tests { use super::*; #[test] fn test_glob_match() { let tests = [ // Test cases from https://research.swtch.com/glob.go ("", "", true), ("x", "", false), ("", "x", false), ("abc", "abc", true), ("*", "abc", true), ("*c", "abc", true), ("*b", "abc", false), ("a*", "abc", true), ("b*", "abc", false), ("a*", "a", true), ("*a", "a", true), ("a*b*c*d*e*", "axbxcxdxe", true), ("a*b*c*d*e*", "axbxcxdxexxx", true), ("*x", "xxx", true), // Test cases from https://github.com/golang/go/blob/master/src/path/filepath/match_test.go ("a*", "ab/c", false), ("a*b", "a/b", false), ("a*/b", "abc/b", true), ("a*/b", "a/c/b", false), ("a*b*c*d*e*/f", "axbxcxdxe/f", true), ("a*b*c*d*e*/f", "axbxcxdxexxx/f", true), ("a*b*c*d*e*/f", "axbxcxdxe/xxx/f", false), ("a*b*c*d*e*/f", "axbxcxdxexxx/fff", false), // Single star (*) // - Empty string ("*", "", true), // - Anything else is covered above // Double star (**) // - Empty string ("**", "", true), ("a**", "a", true), ("**a", "a", true), // - Prefix ("**", "abc", true), ("**", "foo/baz/bar", true), ("**c", "abc", true), ("**b", "abc", false), // - Infix ("a**c", "ac", true), ("a**c", "abc", true), ("a**c", "abd", false), ("a**d", "abc", false), ("a**c", "a/bc", true), ("a**c", "ab/c", true), ("a**c", "a/b/c", true), // -- Infix with left separator ("a/**c", "ac", false), ("a/**c", "a/c", true), ("a/**c", "b/c", false), ("a/**c", "a/d", false), ("a/**c", "a/b/c", true), ("a/**c", "a/b/d", false), ("a/**c", "d/b/c", false), // -- Infix with right separator ("a**/c", "ac", false), ("a**/c", "a/c", true), ("a**/c", "b/c", false), ("a**/c", "a/d", false), ("a**/c", "a/b/c", true), ("a**/c", "a/b/d", false), ("a**/c", "d/b/c", false), // - Infix with two separators ("a/**/c", "ac", false), ("a/**/c", "a/c", true), ("a/**/c", "b/c", false), ("a/**/c", "a/d", false), ("a/**/c", "a/b/c", true), ("a/**/c", "a/b/d", false), ("a/**/c", "d/b/c", false), // - * + * is covered above // - * + ** ("a*b**c", "abc", true), ("a*b**c", "aXbYc", true), ("a*b**c", "aXb/Yc", true), ("a*b**c", "aXbY/Yc", true), ("a*b**c", "aXb/Y/c", true), ("a*b**c", "a/XbYc", false), ("a*b**c", "aX/XbYc", false), ("a*b**c", "a/X/bYc", false), // - ** + * ("a**b*c", "abc", true), ("a**b*c", "aXbYc", true), ("a**b*c", "aXb/Yc", false), ("a**b*c", "aXbY/Yc", false), ("a**b*c", "aXb/Y/c", false), ("a**b*c", "a/XbYc", true), ("a**b*c", "aX/XbYc", true), ("a**b*c", "a/X/bYc", true), // - ** + ** ("a**b**c", "abc", true), ("a**b**c", "aXbYc", true), ("a**b**c", "aXb/Yc", true), ("a**b**c", "aXbY/Yc", true), ("a**b**c", "aXb/Y/c", true), ("a**b**c", "aXbYc", true), ("a**b**c", "a/XbYc", true), ("a**b**c", "aX/XbYc", true), ("a**b**c", "a/X/bYc", true), // Case insensitivity ("*.txt", "file.TXT", true), ("**/*.rs", "dir/file.RS", true), // Optimized patterns: **/*.ext and **/name ("**/*.rs", "foo.rs", true), ("**/*.rs", "dir/foo.rs", true), ("**/*.rs", "dir/sub/foo.rs", true), ("**/*.rs", "foo.txt", false), ("**/*.rs", "dir/foo.txt", false), ("**/Cargo.toml", "Cargo.toml", true), ("**/Cargo.toml", "dir/Cargo.toml", true), ("**/Cargo.toml", "dir/sub/Cargo.toml", true), ("**/Cargo.toml", "Cargo.lock", false), ("**/Cargo.toml", "dir/Cargo.lock", false), ]; for (pattern, name, expected) in tests { let result = glob_match(pattern, name); assert_eq!( result, expected, "test case ({:?}, {:?}, {}) failed, got {}", pattern, name, expected, result ); } } } ================================================ FILE: crates/edit/src/hash.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! Provides fast, non-cryptographic hash functions. use std::hash::Hasher; /// A [`Hasher`] implementation for the wyhash algorithm. /// /// NOTE that you DO NOT want to use this for hashing mere strings/slices. /// The stdlib [`Hash`] implementation for them calls [`Hasher::write`] twice, /// once for the contents and once for a length prefix / `0xff` suffix. #[derive(Default, Clone, Copy)] pub struct WyHash(u64); impl Hasher for WyHash { fn finish(&self) -> u64 { self.0 } fn write(&mut self, bytes: &[u8]) { self.0 = hash(self.0, bytes); } } /// The venerable wyhash hash function. /// /// It's fast, has good statistical properties, and is in the public domain. /// See: /// 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()) } ================================================ FILE: crates/edit/src/helpers.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! Random assortment of helpers I didn't know where to put. use std::cmp::Ordering; use std::io::{self, Read}; use std::mem::MaybeUninit; use std::{fmt, slice}; pub const KILO: usize = 1000; pub const MEGA: usize = 1000 * 1000; pub const GIGA: usize = 1000 * 1000 * 1000; pub const KIBI: usize = 1024; pub const MEBI: usize = 1024 * 1024; pub const GIBI: usize = 1024 * 1024 * 1024; pub struct MetricFormatter(pub T); impl fmt::Display for MetricFormatter { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut value = self.0; let mut suffix = "B"; if value >= GIGA { value /= GIGA; suffix = "GB"; } else if value >= MEGA { value /= MEGA; suffix = "MB"; } else if value >= KILO { value /= KILO; suffix = "kB"; } write!(f, "{value}{suffix}") } } /// A viewport coordinate type used throughout the application. pub type CoordType = isize; /// To avoid overflow issues because you're adding two [`CoordType::MAX`] /// values together, you can use [`COORD_TYPE_SAFE_MAX`] instead. /// /// It equates to half the bits contained in [`CoordType`], which /// for instance is 32767 (0x7FFF) when [`CoordType`] is a [`i32`]. pub const COORD_TYPE_SAFE_MAX: CoordType = (1 << (CoordType::BITS / 2 - 1)) - 1; /// A 2D point. Uses [`CoordType`]. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] pub struct Point { pub x: CoordType, pub y: CoordType, } impl Point { pub const MIN: Self = Self { x: CoordType::MIN, y: CoordType::MIN }; pub const MAX: Self = Self { x: CoordType::MAX, y: CoordType::MAX }; } impl PartialOrd for Point { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for Point { fn cmp(&self, other: &Self) -> Ordering { self.y.cmp(&other.y).then(self.x.cmp(&other.x)) } } /// A 2D size. Uses [`CoordType`]. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] pub struct Size { pub width: CoordType, pub height: CoordType, } impl Size { pub fn as_rect(&self) -> Rect { Rect { left: 0, top: 0, right: self.width, bottom: self.height } } } /// A 2D rectangle. Uses [`CoordType`]. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] pub struct Rect { pub left: CoordType, pub top: CoordType, pub right: CoordType, pub bottom: CoordType, } impl Rect { /// Mimics CSS's `padding` property where `padding: a` is `a a a a`. pub fn one(value: CoordType) -> Self { Self { left: value, top: value, right: value, bottom: value } } /// Mimics CSS's `padding` property where `padding: a b` is `a b a b`, /// and `a` is top/bottom and `b` is left/right. pub fn two(top_bottom: CoordType, left_right: CoordType) -> Self { Self { left: left_right, top: top_bottom, right: left_right, bottom: top_bottom } } /// Mimics CSS's `padding` property where `padding: a b c` is `a b c b`, /// and `a` is top, `b` is left/right, and `c` is bottom. pub fn three(top: CoordType, left_right: CoordType, bottom: CoordType) -> Self { Self { left: left_right, top, right: left_right, bottom } } /// Is the rectangle empty? pub fn is_empty(&self) -> bool { self.left >= self.right || self.top >= self.bottom } /// Width of the rectangle. pub fn width(&self) -> CoordType { self.right - self.left } /// Height of the rectangle. pub fn height(&self) -> CoordType { self.bottom - self.top } /// Check if it contains a point. pub fn contains(&self, point: Point) -> bool { point.x >= self.left && point.x < self.right && point.y >= self.top && point.y < self.bottom } /// Intersect two rectangles. pub fn intersect(&self, rhs: Self) -> Self { let l = self.left.max(rhs.left); let t = self.top.max(rhs.top); let r = self.right.min(rhs.right); let b = self.bottom.min(rhs.bottom); // Ensure that the size is non-negative. This avoids bugs, // because some height/width is negative all of a sudden. let r = l.max(r); let b = t.max(b); Self { left: l, top: t, right: r, bottom: b } } } /// [`Read`] but with [`MaybeUninit`] buffers. pub fn file_read_uninit(file: &mut T, buf: &mut [MaybeUninit]) -> io::Result { 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) } } ================================================ FILE: crates/edit/src/icu.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! Bindings to the ICU library. use std::cmp::Ordering; use std::ffi::{CStr, c_char}; use std::mem::MaybeUninit; use std::ops::Range; use std::ptr::{null, null_mut}; use std::{fmt, mem}; use stdext::arena::{Arena, scratch_arena}; use stdext::arena_format; use stdext::collections::{BString, BVec}; use stdext::unicode::Utf8Chars; use crate::buffer::TextBuffer; use crate::sys; pub(crate) const ILLEGAL_ARGUMENT_ERROR: Error = Error(1); // U_ILLEGAL_ARGUMENT_ERROR pub const ICU_MISSING_ERROR: Error = Error(0); #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Error(u32); impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn format(code: u32) -> &'static str { let Ok(f) = init_if_needed() else { return ""; }; let status = icu_ffi::UErrorCode::new(code); let ptr = unsafe { (f.u_errorName)(status) }; if ptr.is_null() { return ""; } let str = unsafe { CStr::from_ptr(ptr) }; str.to_str().unwrap_or("") } let code = self.0; if code != 0 && let msg = format(code) && !msg.is_empty() { write!(f, "ICU Error: {msg}") } else { write!(f, "ICU Error: {code:#08x}") } } } pub type Result = std::result::Result; #[derive(Clone, Copy)] pub struct Encoding { pub label: &'static str, pub canonical: &'static str, } pub struct Encodings { pub preferred: &'static [Encoding], pub all: &'static [Encoding], } static mut ENCODINGS: Encodings = Encodings { preferred: &[], all: &[] }; /// Returns a list of encodings ICU supports. pub fn get_available_encodings() -> &'static Encodings { // OnceCell for people that want to put it into a static. #[allow(static_mut_refs)] unsafe { if ENCODINGS.all.is_empty() { let scratch = scratch_arena(None); let mut preferred = BVec::empty(); let mut alternative = BVec::empty(); // These encodings are always available. preferred.push(&*scratch, Encoding { label: "UTF-8", canonical: "UTF-8" }); preferred.push(&*scratch, Encoding { label: "UTF-8 BOM", canonical: "UTF-8 BOM" }); if let Ok(f) = init_if_needed() { let mut n = 0; loop { let name = (f.ucnv_getAvailableName)(n); if name.is_null() { break; } n += 1; let name = CStr::from_ptr(name).to_str().unwrap_unchecked(); // We have already pushed UTF-8 above and can skip it. // There is no need to filter UTF-8 BOM here, // since ICU does not distinguish it from UTF-8. if name.is_empty() || name == "UTF-8" { continue; } let mut status = icu_ffi::U_ZERO_ERROR; let mime = (f.ucnv_getStandardName)( name.as_ptr(), c"MIME".as_ptr() as *const _, &mut status, ); if !mime.is_null() && status.is_success() { let mime = CStr::from_ptr(mime).to_str().unwrap_unchecked(); preferred.push(&*scratch, Encoding { label: mime, canonical: name }); } else { alternative.push(&*scratch, Encoding { label: name, canonical: name }); } } } let preferred_len = preferred.len(); // Combine the preferred and alternative encodings into a single list. let mut all = Vec::with_capacity(preferred.len() + alternative.len()); all.extend(preferred); all.extend(alternative); let all = all.leak(); ENCODINGS.preferred = &all[..preferred_len]; ENCODINGS.all = &all[..]; } &ENCODINGS } } /// Converts between two encodings using ICU. pub struct Converter<'pivot> { source: *mut icu_ffi::UConverter, target: *mut icu_ffi::UConverter, pivot_buffer: &'pivot mut [MaybeUninit], pivot_source: *mut u16, pivot_target: *mut u16, reset: bool, } impl Drop for Converter<'_> { fn drop(&mut self) { let f = assume_loaded(); unsafe { (f.ucnv_close)(self.source) }; unsafe { (f.ucnv_close)(self.target) }; } } impl<'pivot> Converter<'pivot> { /// Constructs a new `Converter` instance. /// /// # Parameters /// /// * `pivot_buffer`: A buffer used to cache partial conversions. /// Don't make it too small. /// * `source_encoding`: The source encoding name (e.g., "UTF-8"). /// * `target_encoding`: The target encoding name (e.g., "UTF-16"). pub fn new( pivot_buffer: &'pivot mut [MaybeUninit], source_encoding: &str, target_encoding: &str, ) -> Result { let f = init_if_needed()?; let arena = scratch_arena(None); let source_encoding = Self::append_nul(&arena, source_encoding); let target_encoding = Self::append_nul(&arena, target_encoding); let mut status = icu_ffi::U_ZERO_ERROR; let source = unsafe { (f.ucnv_open)(source_encoding.as_ptr(), &mut status) }; let target = unsafe { (f.ucnv_open)(target_encoding.as_ptr(), &mut status) }; if status.is_failure() { if !source.is_null() { unsafe { (f.ucnv_close)(source) }; } if !target.is_null() { unsafe { (f.ucnv_close)(target) }; } return Err(status.as_error()); } let pivot_source = pivot_buffer.as_mut_ptr() as *mut u16; let pivot_target = unsafe { pivot_source.add(pivot_buffer.len()) }; Ok(Self { source, target, pivot_buffer, pivot_source, pivot_target, reset: true }) } fn append_nul<'a>(arena: &'a Arena, input: &str) -> BString<'a> { arena_format!(arena, "{}\0", input) } /// Performs one step of the encoding conversion. /// /// # Parameters /// /// * `input`: The input buffer to convert from. /// It should be in the `source_encoding` that was previously specified. /// * `output`: The output buffer to convert to. /// It should be in the `target_encoding` that was previously specified. /// /// # Returns /// /// A tuple containing: /// 1. The number of bytes read from the input buffer. /// 2. The number of bytes written to the output buffer. pub fn convert( &mut self, input: &[u8], output: &mut [MaybeUninit], ) -> Result<(usize, usize)> { let f = assume_loaded(); let input_beg = input.as_ptr(); let input_end = unsafe { input_beg.add(input.len()) }; let mut input_ptr = input_beg; let output_beg = output.as_mut_ptr() as *mut u8; let output_end = unsafe { output_beg.add(output.len()) }; let mut output_ptr = output_beg; let pivot_beg = self.pivot_buffer.as_mut_ptr() as *mut u16; let pivot_end = unsafe { pivot_beg.add(self.pivot_buffer.len()) }; let flush = input.is_empty(); let mut status = icu_ffi::U_ZERO_ERROR; unsafe { (f.ucnv_convertEx)( /* target_cnv */ self.target, /* source_cnv */ self.source, /* target */ &mut output_ptr, /* target_limit */ output_end, /* source */ &mut input_ptr, /* source_limit */ input_end, /* pivot_start */ pivot_beg, /* pivot_source */ &mut self.pivot_source, /* pivot_target */ &mut self.pivot_target, /* pivot_limit */ pivot_end, /* reset */ self.reset, /* flush */ flush, /* status */ &mut status, ); } self.reset = false; if status.is_failure() && status != icu_ffi::U_BUFFER_OVERFLOW_ERROR { return Err(status.as_error()); } let input_advance = unsafe { input_ptr.offset_from(input_beg) as usize }; let output_advance = unsafe { output_ptr.offset_from(output_beg) as usize }; Ok((input_advance, output_advance)) } } // In benchmarking, I found that the performance does not really change much by changing this value. // I picked 64 because it seemed like a reasonable lower bound. const CACHE_SIZE: usize = 64; /// Caches a chunk of TextBuffer contents (UTF-8) in UTF-16 format. #[repr(C)] struct Cache { /// The translated text. Contains [`Cache::utf16_len`]-many valid items. utf16: [u16; CACHE_SIZE], /// For each character in [`Cache::utf16`] this stores the offset in the [`TextBuffer`], /// relative to the start offset stored in `native_beg`. /// This has the same length as [`Cache::utf16`]. utf16_to_utf8_offsets: [u16; CACHE_SIZE], /// `utf8_to_utf16_offsets[native_offset - native_beg]` will tell you which character in /// [`Cache::utf16`] maps to the given `native_offset` in the underlying [`TextBuffer`]. /// Contains `native_end - native_beg`-many valid items. utf8_to_utf16_offsets: [u16; CACHE_SIZE], /// The number of valid items in [`Cache::utf16`]. utf16_len: usize, /// Offset of the first non-ASCII character. /// Less than or equal to [`Cache::utf16_len`]. native_indexing_limit: usize, /// The range of UTF-8 text in the [`TextBuffer`] that this chunk covers. utf8_range: Range, } #[repr(C)] struct DoubleCache { cache: [Cache; 2], /// You can consider this a 1 bit index into `cache`. mru: bool, } /// A wrapper around ICU's `UText` struct. /// /// In our case its only purpose is to adapt a [`TextBuffer`] for ICU. /// /// # Safety /// /// Warning! No lifetime tracking is done here. /// I initially did it properly with a PhantomData marker for the TextBuffer /// lifetime, but it was a pain so now I don't. Not a big deal in our case. pub struct Text(&'static mut icu_ffi::UText); impl Drop for Text { fn drop(&mut self) { let f = assume_loaded(); unsafe { (f.utext_close)(self.0) }; } } impl Text { /// Constructs an ICU `UText` instance from a [`TextBuffer`]. /// /// # Safety /// /// The caller must ensure that the given [`TextBuffer`] /// outlives the returned `Text` instance. pub unsafe fn new(tb: &TextBuffer) -> Result { let f = init_if_needed()?; let mut status = icu_ffi::U_ZERO_ERROR; let ptr = unsafe { (f.utext_setup)(null_mut(), size_of::() as i32, &mut status) }; if status.is_failure() { return Err(status.as_error()); } const FUNCS: icu_ffi::UTextFuncs = icu_ffi::UTextFuncs { table_size: size_of::() as i32, reserved1: 0, reserved2: 0, reserved3: 0, clone: Some(utext_clone), native_length: Some(utext_native_length), access: Some(utext_access), extract: None, replace: None, copy: None, map_offset_to_native: Some(utext_map_offset_to_native), map_native_index_to_utf16: Some(utext_map_native_index_to_utf16), close: None, spare1: None, spare2: None, spare3: None, }; let ut = unsafe { &mut *ptr }; ut.p_funcs = &FUNCS; ut.context = tb as *const TextBuffer as *mut _; ut.a = -1; Ok(Self(ut)) } } fn text_buffer_from_utext<'a>(ut: &icu_ffi::UText) -> &'a TextBuffer { unsafe { &*(ut.context as *const TextBuffer) } } fn double_cache_from_utext<'a>(ut: &icu_ffi::UText) -> &'a mut DoubleCache { unsafe { &mut *(ut.p_extra as *mut DoubleCache) } } extern "C" fn utext_clone( dest: *mut icu_ffi::UText, src: &icu_ffi::UText, deep: bool, status: &mut icu_ffi::UErrorCode, ) -> *mut icu_ffi::UText { if status.is_failure() { return null_mut(); } if deep { *status = icu_ffi::U_UNSUPPORTED_ERROR; return null_mut(); } let f = assume_loaded(); let ut_ptr = unsafe { (f.utext_setup)(dest, size_of::() as i32, status) }; if status.is_failure() { return null_mut(); } // TODO: I'm somewhat unsure whether we have to preserve the `chunk_offset`. // We can't blindly copy chunk contents and the `Cache` in `ut.p_extra`, // because they may contain dirty contents (different `TextBuffer` generation). unsafe { let ut = &mut *ut_ptr; ut.p_funcs = src.p_funcs; ut.context = src.context; ut.a = -1; } ut_ptr } extern "C" fn utext_native_length(ut: &mut icu_ffi::UText) -> i64 { let tb = text_buffer_from_utext(ut); tb.text_length() as i64 } extern "C" fn utext_access(ut: &mut icu_ffi::UText, native_index: i64, forward: bool) -> bool { if let Some(cache) = utext_access_impl(ut, native_index, forward) { let native_off = native_index as usize - cache.utf8_range.start; ut.chunk_contents = cache.utf16.as_ptr(); ut.chunk_length = cache.utf16_len as i32; ut.chunk_offset = cache.utf8_to_utf16_offsets[native_off] as i32; ut.chunk_native_start = cache.utf8_range.start as i64; ut.chunk_native_limit = cache.utf8_range.end as i64; ut.native_indexing_limit = cache.native_indexing_limit as i32; true } else { false } } fn utext_access_impl<'a>( ut: &mut icu_ffi::UText, native_index: i64, forward: bool, ) -> Option<&'a mut Cache> { let tb = text_buffer_from_utext(ut); let mut index_contained = native_index; if !forward { index_contained -= 1; } if index_contained < 0 || index_contained as usize >= tb.text_length() { return None; } let index_contained = index_contained as usize; let native_index = native_index as usize; let double_cache = double_cache_from_utext(ut); let dirty = ut.a != tb.generation() as i64; if dirty { // The text buffer contents have changed. // Invalidate both caches so that future calls don't mistakenly use them // when they enter the for loop in the else branch below (`dirty == false`). double_cache.cache[0].utf16_len = 0; double_cache.cache[1].utf16_len = 0; double_cache.cache[0].utf8_range = 0..0; double_cache.cache[1].utf8_range = 0..0; ut.a = tb.generation() as i64; } else { // Check if one of the caches already contains the requested range. for (i, cache) in double_cache.cache.iter_mut().enumerate() { if cache.utf8_range.contains(&index_contained) { double_cache.mru = i != 0; return Some(cache); } } } // Turn the least recently used cache into the most recently used one. let double_cache = double_cache_from_utext(ut); double_cache.mru = !double_cache.mru; let cache = &mut double_cache.cache[double_cache.mru as usize]; // In order to safely fit any UTF-8 character into our cache, // we must assume the worst case of a 4-byte long encoding. const UTF16_LEN_LIMIT: usize = CACHE_SIZE - 4; let utf8_len_limit; let native_start; if forward { utf8_len_limit = (tb.text_length() - native_index).min(UTF16_LEN_LIMIT); native_start = native_index; } else { // The worst case ratio for UTF-8 to UTF-16 is 1:1, when the text is ASCII. // This allows us to safely subtract the UTF-16 buffer size // and assume that whatever we read as UTF-8 will fit. // TODO: Test what happens if you have lots of invalid UTF-8 text blow up to U+FFFD. utf8_len_limit = native_index.min(UTF16_LEN_LIMIT); // Since simply subtracting an offset may end up in the middle of a codepoint sequence, // we must align the offset to the next codepoint boundary. // Here we skip trail bytes until we find a lead. let mut beg = native_index - utf8_len_limit; let chunk = tb.read_forward(beg); for &c in chunk { if c & 0b1100_0000 != 0b1000_0000 { break; } beg += 1; } native_start = beg; } // Translate the given range from UTF-8 to UTF-16. // NOTE: This code makes the assumption that the `native_index` is always // at UTF-8 codepoint boundaries which technically isn't guaranteed. let mut utf16_len = 0; let mut utf8_len = 0; let mut ascii_len = 0; 'outer: loop { let initial_utf8_len = utf8_len; let chunk = tb.read_forward(native_start + utf8_len); if chunk.is_empty() { break; } let mut it = Utf8Chars::new(chunk, 0); // If we've only seen ASCII so far we can fast-pass the UTF-16 translation, // because we can just widen from u8 -> u16. if utf16_len == ascii_len { let haystack = &chunk[..chunk.len().min(utf8_len_limit - ascii_len)]; // When it comes to performance, and the search space is small (which it is here), // it's always a good idea to keep the loops small and tight... let len = haystack.iter().position(|&c| c >= 0x80).unwrap_or(haystack.len()); // ...In this case it allows the compiler to vectorize this loop and double // the performance. Luckily, llvm doesn't unroll the loop, which is great, // because `len` will always be a relatively small number. for &c in &chunk[..len] { unsafe { *cache.utf16.get_unchecked_mut(ascii_len) = c as u16; *cache.utf16_to_utf8_offsets.get_unchecked_mut(ascii_len) = ascii_len as u16; *cache.utf8_to_utf16_offsets.get_unchecked_mut(ascii_len) = ascii_len as u16; } ascii_len += 1; } utf16_len += len; utf8_len += len; it.seek(len); if ascii_len >= UTF16_LEN_LIMIT { break; } } while let Some(c) = it.next() { // Thanks to our `if utf16_len >= UTF16_LEN_LIMIT` check, // we can safely assume that this will fit. unsafe { let utf8_len_beg = utf8_len; let utf8_len_end = initial_utf8_len + it.offset(); while utf8_len < utf8_len_end { *cache.utf8_to_utf16_offsets.get_unchecked_mut(utf8_len) = utf16_len as u16; utf8_len += 1; } if c <= '\u{FFFF}' { *cache.utf16.get_unchecked_mut(utf16_len) = c as u16; *cache.utf16_to_utf8_offsets.get_unchecked_mut(utf16_len) = utf8_len_beg as u16; utf16_len += 1; } else { let c = c as u32 - 0x10000; let b = utf8_len_beg as u16; *cache.utf16.get_unchecked_mut(utf16_len) = (c >> 10) as u16 | 0xD800; *cache.utf16.get_unchecked_mut(utf16_len + 1) = (c & 0x3FF) as u16 | 0xDC00; *cache.utf16_to_utf8_offsets.get_unchecked_mut(utf16_len) = b; *cache.utf16_to_utf8_offsets.get_unchecked_mut(utf16_len + 1) = b; utf16_len += 2; } } if utf16_len >= UTF16_LEN_LIMIT || utf8_len >= utf8_len_limit { break 'outer; } } } // Allow for looking up past-the-end indices via // `utext_map_offset_to_native` and `utext_map_native_index_to_utf16`. cache.utf16_to_utf8_offsets[utf16_len] = utf8_len as u16; cache.utf8_to_utf16_offsets[utf8_len] = utf16_len as u16; let native_limit = native_start + utf8_len; cache.utf16_len = utf16_len; // If parts of the UTF-8 chunk are ASCII, we can tell ICU that it doesn't need to call // utext_map_offset_to_native. For some reason, uregex calls that function *a lot*, // literally half the CPU time is spent on it. cache.native_indexing_limit = ascii_len; cache.utf8_range = native_start..native_limit; Some(cache) } extern "C" fn utext_map_offset_to_native(ut: &icu_ffi::UText) -> i64 { debug_assert!((0..=ut.chunk_length).contains(&ut.chunk_offset)); let double_cache = double_cache_from_utext(ut); let cache = &double_cache.cache[double_cache.mru as usize]; let off_rel = cache.utf16_to_utf8_offsets[ut.chunk_offset as usize]; let off_abs = cache.utf8_range.start + off_rel as usize; off_abs as i64 } extern "C" fn utext_map_native_index_to_utf16(ut: &icu_ffi::UText, native_index: i64) -> i32 { debug_assert!((ut.chunk_native_start..=ut.chunk_native_limit).contains(&native_index)); let double_cache = double_cache_from_utext(ut); let cache = &double_cache.cache[double_cache.mru as usize]; let off_rel = cache.utf8_to_utf16_offsets[(native_index - ut.chunk_native_start) as usize]; off_rel as i32 } /// A wrapper around ICU's `URegularExpression` struct. /// /// # Safety /// /// Warning! No lifetime tracking is done here. pub struct Regex(&'static mut icu_ffi::URegularExpression); impl Drop for Regex { fn drop(&mut self) { let f = assume_loaded(); unsafe { (f.uregex_close)(self.0) }; } } impl Regex { /// Enable case-insensitive matching. pub const CASE_INSENSITIVE: i32 = icu_ffi::UREGEX_CASE_INSENSITIVE; /// If set, ^ and $ match the start and end of each line. /// Otherwise, they match the start and end of the entire string. pub const MULTILINE: i32 = icu_ffi::UREGEX_MULTILINE; /// Treat the given pattern as a literal string. pub const LITERAL: i32 = icu_ffi::UREGEX_LITERAL; /// Constructs a regex, plain and simple. Read `uregex_open` docs. /// /// # Safety /// /// The caller must ensure that the given `Text` outlives the returned `Regex` instance. pub unsafe fn new(pattern: &str, flags: i32, text: &Text) -> Result { let f = init_if_needed()?; unsafe { let scratch = scratch_arena(None); let mut utf16 = BVec::empty(); let mut status = icu_ffi::U_ZERO_ERROR; utf16.extend_sloppy(&*scratch, pattern.encode_utf16()); let ptr = (f.uregex_open)( utf16.as_ptr(), utf16.len() as i32, icu_ffi::UREGEX_MULTILINE | icu_ffi::UREGEX_ERROR_ON_UNKNOWN_ESCAPES | flags, None, &mut status, ); // ICU describes the time unit as being dependent on CPU performance // and "typically [in] the order of milliseconds", but this claim seems // highly outdated. On my CPU from 2021, a limit of 4096 equals roughly 600ms. (f.uregex_setTimeLimit)(ptr, 4096, &mut status); (f.uregex_setUText)(ptr, text.0 as *const _ as *mut _, &mut status); if status.is_failure() { return Err(status.as_error()); } Ok(Self(&mut *ptr)) } } /// Updates the regex pattern with the given text. /// If the text contents have changed, you can pass the same text as you used /// initially and it'll trigger ICU to reload the text and invalidate its caches. /// /// # Safety /// /// The caller must ensure that the given `Text` outlives the `Regex` instance. pub unsafe fn set_text(&mut self, text: &mut Text, offset: usize) { // Get `utext_access_impl` to detect the `TextBuffer::generation` change, // and refresh its contents. This ensures that ICU doesn't reuse // stale `UText::chunk_contents`, as it has no way tell that it's stale. utext_access(text.0, offset as i64, true); let f = assume_loaded(); let mut status = icu_ffi::U_ZERO_ERROR; unsafe { (f.uregex_setUText)(self.0, text.0 as *const _ as *mut _, &mut status) }; // `uregex_setUText` resets the regex to the start of the text. // Because of this, we must also call `uregex_reset64`. unsafe { (f.uregex_reset64)(self.0, offset as i64, &mut status) }; } /// Sets the regex to the absolute offset in the underlying text. pub fn reset(&mut self, offset: usize) { let f = assume_loaded(); let mut status = icu_ffi::U_ZERO_ERROR; unsafe { (f.uregex_reset64)(self.0, offset as i64, &mut status) }; } /// Gets captured group count. pub fn group_count(&mut self) -> i32 { let f = assume_loaded(); let mut status = icu_ffi::U_ZERO_ERROR; let count = unsafe { (f.uregex_groupCount)(self.0, &mut status) }; if status.is_failure() { 0 } else { count } } /// Gets the text range of a captured group by index. pub fn group(&mut self, group: i32) -> Option> { let f = assume_loaded(); let mut status = icu_ffi::U_ZERO_ERROR; let start = unsafe { (f.uregex_start64)(self.0, group, &mut status) }; let end = unsafe { (f.uregex_end64)(self.0, group, &mut status) }; if status.is_failure() { None } else { let start = start.max(0); let end = end.max(start); Some(start as usize..end as usize) } } } impl Iterator for Regex { type Item = Range; fn next(&mut self) -> Option { let f = assume_loaded(); let mut status = icu_ffi::U_ZERO_ERROR; let ok = unsafe { (f.uregex_findNext)(self.0, &mut status) }; if !ok { return None; } self.group(0) } } static mut ROOT_COLLATOR: Option<*mut icu_ffi::UCollator> = None; /// Compares two UTF-8 strings for sorting using ICU's collation algorithm. pub fn compare_strings(a: &[u8], b: &[u8]) -> Ordering { #[cold] fn init() { unsafe { let mut coll = null_mut(); if let Ok(f) = init_if_needed() { let mut status = icu_ffi::U_ZERO_ERROR; coll = (f.ucol_open)(c"".as_ptr(), &mut status); // Turns on Unicode normalization. I'm not 100% sure if it's needed, but it only has a // small-ish performance impact and sounds like it's required for correct filename sorting. (f.ucol_setAttribute)( coll, icu_ffi::UCOL_NORMALIZATION_MODE, icu_ffi::UCOL_ON, &mut status, ); // Ensure that "file2" < "file10", even though '2' > '1'. // NOTE: This has a _huge_ performance impact. It's roughly 5x slower for our purpose of // sorting filenames. If it becomes an issue, we could use `ucol_getSortKey` (only +25%). // (`ucol_strcollUTF8` is faster if `UCOL_NUMERIC_COLLATION` isn't used.) (f.ucol_setAttribute)( coll, icu_ffi::UCOL_NUMERIC_COLLATION, icu_ffi::UCOL_ON, &mut status, ); if status.is_failure() { coll = null_mut(); } } ROOT_COLLATOR = Some(coll); } } // OnceCell for people that want to put it into a static. #[allow(static_mut_refs)] let coll = unsafe { if ROOT_COLLATOR.is_none() { init(); } ROOT_COLLATOR.unwrap_unchecked() }; if coll.is_null() { compare_strings_ascii(a, b) } else { let f = assume_loaded(); let mut status = icu_ffi::U_ZERO_ERROR; let res = unsafe { (f.ucol_strcollUTF8)( coll, a.as_ptr(), a.len() as i32, b.as_ptr(), b.len() as i32, &mut status, ) }; match res { icu_ffi::UCollationResult::UCOL_EQUAL => Ordering::Equal, icu_ffi::UCollationResult::UCOL_GREATER => Ordering::Greater, icu_ffi::UCollationResult::UCOL_LESS => Ordering::Less, } } } /// Unicode collation via `ucol_strcollUTF8`, now for ASCII! fn compare_strings_ascii(a: &[u8], b: &[u8]) -> Ordering { let mut iter = a.iter().zip(b.iter()); // Low weight: Find the first character which differs. // // Remember that result in case all remaining characters are // case-insensitive equal, because then we use that as a fallback. while let Some((&a, &b)) = iter.next() { if a != b { let mut order = a.cmp(&b); let la = a.to_ascii_lowercase(); let lb = b.to_ascii_lowercase(); if la == lb { // High weight: Find the first character which // differs case-insensitively. for (a, b) in iter { let la = a.to_ascii_lowercase(); let lb = b.to_ascii_lowercase(); if la != lb { order = la.cmp(&lb); break; } } } return order; } } // Fallback: The shorter string wins. a.len().cmp(&b.len()) } static mut ROOT_CASEMAP: Option<*mut icu_ffi::UCaseMap> = None; /// Converts the given UTF-8 string to lower case. /// /// Case folding differs from lower case in that the output is primarily useful /// to machines for comparisons. It's like applying Unicode normalization. pub fn fold_case<'a>(arena: &'a Arena, input: &str) -> BString<'a> { // OnceCell for people that want to put it into a static. #[allow(static_mut_refs)] let casemap = unsafe { if ROOT_CASEMAP.is_none() { ROOT_CASEMAP = Some(if let Ok(f) = init_if_needed() { let mut status = icu_ffi::U_ZERO_ERROR; (f.ucasemap_open)(null(), 0, &mut status) } else { null_mut() }) } ROOT_CASEMAP.unwrap_unchecked() }; if !casemap.is_null() { let f = assume_loaded(); let mut status = icu_ffi::U_ZERO_ERROR; let mut output = BVec::empty(); let mut output_len; // First, guess the output length: // TODO: What's a good heuristic here? { output.reserve_exact(arena, input.len() + 16); let output = output.spare_capacity_mut(); output_len = unsafe { (f.ucasemap_utf8FoldCase)( casemap, output.as_mut_ptr() as *mut _, output.len() as i32, input.as_ptr() as *const _, input.len() as i32, &mut status, ) }; } // If that failed to fit, retry with the correct length. if status == icu_ffi::U_BUFFER_OVERFLOW_ERROR && output_len > 0 { output.reserve_exact(arena, output_len as usize); let output = output.spare_capacity_mut(); output_len = unsafe { (f.ucasemap_utf8FoldCase)( casemap, output.as_mut_ptr() as *mut _, output.len() as i32, input.as_ptr() as *const _, input.len() as i32, &mut status, ) }; } if status.is_success() && output_len > 0 { unsafe { output.set_len(output_len as usize); } return unsafe { BString::from_utf8_unchecked(output) }; } } let mut result = BString::from_str(arena, input); for b in unsafe { result.as_bytes_mut() } { b.make_ascii_lowercase(); } result } // NOTE: // To keep this neat, fields are ordered by prefix (= `ucol_` before `uregex_`), // followed by functions in this order: // * Static methods (e.g. `ucnv_getAvailableName`) // * Constructors (e.g. `ucnv_open`) // * Destructors (e.g. `ucnv_close`) // * Methods, grouped by relationship // (e.g. `uregex_start64` and `uregex_end64` are near each other) // // WARNING: // The order of the fields MUST match the order of strings in the following two arrays. #[allow(non_snake_case)] #[repr(C)] struct LibraryFunctions { // LIBICUUC_PROC_NAMES u_errorName: icu_ffi::u_errorName, ucasemap_open: icu_ffi::ucasemap_open, ucasemap_utf8FoldCase: icu_ffi::ucasemap_utf8FoldCase, ucnv_getAvailableName: icu_ffi::ucnv_getAvailableName, ucnv_getStandardName: icu_ffi::ucnv_getStandardName, ucnv_open: icu_ffi::ucnv_open, ucnv_close: icu_ffi::ucnv_close, ucnv_convertEx: icu_ffi::ucnv_convertEx, utext_setup: icu_ffi::utext_setup, utext_close: icu_ffi::utext_close, // LIBICUI18N_PROC_NAMES ucol_open: icu_ffi::ucol_open, ucol_setAttribute: icu_ffi::ucol_setAttribute, ucol_strcollUTF8: icu_ffi::ucol_strcollUTF8, uregex_open: icu_ffi::uregex_open, uregex_close: icu_ffi::uregex_close, uregex_setTimeLimit: icu_ffi::uregex_setTimeLimit, uregex_setUText: icu_ffi::uregex_setUText, uregex_reset64: icu_ffi::uregex_reset64, uregex_findNext: icu_ffi::uregex_findNext, uregex_groupCount: icu_ffi::uregex_groupCount, uregex_start64: icu_ffi::uregex_start64, uregex_end64: icu_ffi::uregex_end64, } macro_rules! proc_name { ($s:literal) => { concat!(env!("EDIT_CFG_ICU_EXPORT_PREFIX"), $s, env!("EDIT_CFG_ICU_EXPORT_SUFFIX"), "\0") .as_ptr() as *const c_char }; } // Found in libicuuc.so on UNIX, icuuc.dll/icu.dll on Windows. const LIBICUUC_PROC_NAMES: [*const c_char; 10] = [ proc_name!("u_errorName"), proc_name!("ucasemap_open"), proc_name!("ucasemap_utf8FoldCase"), proc_name!("ucnv_getAvailableName"), proc_name!("ucnv_getStandardName"), proc_name!("ucnv_open"), proc_name!("ucnv_close"), proc_name!("ucnv_convertEx"), proc_name!("utext_setup"), proc_name!("utext_close"), ]; // Found in libicui18n.so on UNIX, icuin.dll/icu.dll on Windows. const LIBICUI18N_PROC_NAMES: [*const c_char; 12] = [ proc_name!("ucol_open"), proc_name!("ucol_setAttribute"), proc_name!("ucol_strcollUTF8"), proc_name!("uregex_open"), proc_name!("uregex_close"), proc_name!("uregex_setTimeLimit"), proc_name!("uregex_setUText"), proc_name!("uregex_reset64"), proc_name!("uregex_findNext"), proc_name!("uregex_groupCount"), proc_name!("uregex_start64"), proc_name!("uregex_end64"), ]; enum LibraryFunctionsState { Uninitialized, Failed, Loaded(LibraryFunctions), } static mut LIBRARY_FUNCTIONS: LibraryFunctionsState = LibraryFunctionsState::Uninitialized; pub fn init() -> Result<()> { init_if_needed()?; Ok(()) } #[allow(static_mut_refs)] fn init_if_needed() -> Result<&'static LibraryFunctions> { #[cold] fn load() { unsafe { LIBRARY_FUNCTIONS = LibraryFunctionsState::Failed; let Ok(icu) = sys::load_icu() else { return; }; type TransparentFunction = unsafe extern "C" fn() -> *const (); // OH NO I'M DOING A BAD THING // // If this assertion hits, you either forgot to update `LIBRARY_PROC_NAMES` // or you're on a platform where `dlsym` behaves different from classic UNIX and Windows. // // This code assumes that we can treat the `LibraryFunctions` struct containing various different function // pointers as an array of `TransparentFunction` pointers. In C, this works on any platform that supports // POSIX `dlsym` or equivalent, but I suspect Rust is once again being extra about it. In any case, that's // still better than loading every function one by one, just to blow up our binary size for no reason. const _: () = assert!( mem::size_of::() == mem::size_of::() * (LIBICUUC_PROC_NAMES.len() + LIBICUI18N_PROC_NAMES.len()) ); let mut funcs = MaybeUninit::::uninit(); let mut ptr = funcs.as_mut_ptr() as *mut TransparentFunction; #[cfg(edit_icu_renaming_auto_detect)] let scratch_outer = scratch_arena(None); #[cfg(edit_icu_renaming_auto_detect)] let suffix = sys::icu_detect_renaming_suffix(&scratch_outer, icu.libicuuc); for (handle, names) in [ (icu.libicuuc, &LIBICUUC_PROC_NAMES[..]), (icu.libicui18n, &LIBICUI18N_PROC_NAMES[..]), ] { for &name in names { #[cfg(edit_icu_renaming_auto_detect)] let scratch = scratch_arena(Some(&scratch_outer)); #[cfg(edit_icu_renaming_auto_detect)] let name = sys::icu_add_renaming_suffix(&scratch, name, &suffix); let Ok(func) = sys::get_proc_address(handle, name) else { debug_assert!( false, "Failed to load ICU function: {:?}", CStr::from_ptr(name) ); return; }; ptr.write(func); ptr = ptr.add(1); } } LIBRARY_FUNCTIONS = LibraryFunctionsState::Loaded(funcs.assume_init()); } } unsafe { if matches!(&LIBRARY_FUNCTIONS, LibraryFunctionsState::Uninitialized) { load(); } } match unsafe { &LIBRARY_FUNCTIONS } { LibraryFunctionsState::Loaded(f) => Ok(f), _ => Err(ICU_MISSING_ERROR), } } #[allow(static_mut_refs)] fn assume_loaded() -> &'static LibraryFunctions { match unsafe { &LIBRARY_FUNCTIONS } { LibraryFunctionsState::Loaded(f) => f, _ => unreachable!(), } } mod icu_ffi { #![allow(dead_code, non_camel_case_types)] use std::ffi::{c_char, c_int, c_void}; use super::Error; #[derive(Copy, Clone, Eq, PartialEq)] #[repr(transparent)] pub struct UErrorCode(c_int); impl UErrorCode { pub const fn new(code: u32) -> Self { Self(code as c_int) } pub fn is_success(&self) -> bool { self.0 <= 0 } pub fn is_failure(&self) -> bool { self.0 > 0 } pub fn as_error(&self) -> Error { debug_assert!(self.0 > 0); Error(self.0 as u32) } } pub const U_ZERO_ERROR: UErrorCode = UErrorCode(0); pub const U_BUFFER_OVERFLOW_ERROR: UErrorCode = UErrorCode(15); pub const U_UNSUPPORTED_ERROR: UErrorCode = UErrorCode(16); pub type u_errorName = unsafe extern "C" fn(code: UErrorCode) -> *const c_char; pub struct UConverter; pub type ucnv_getAvailableName = unsafe extern "C" fn(n: i32) -> *const c_char; pub type ucnv_getStandardName = unsafe extern "C" fn( name: *const u8, standard: *const u8, status: &mut UErrorCode, ) -> *const c_char; pub type ucnv_open = unsafe extern "C" fn(converter_name: *const u8, status: &mut UErrorCode) -> *mut UConverter; pub type ucnv_close = unsafe extern "C" fn(converter: *mut UConverter); pub type ucnv_convertEx = unsafe extern "C" fn( target_cnv: *mut UConverter, source_cnv: *mut UConverter, target: *mut *mut u8, target_limit: *const u8, source: *mut *const u8, source_limit: *const u8, pivot_start: *mut u16, pivot_source: *mut *mut u16, pivot_target: *mut *mut u16, pivot_limit: *const u16, reset: bool, flush: bool, status: &mut UErrorCode, ); pub struct UCaseMap; pub type ucasemap_open = unsafe extern "C" fn( locale: *const c_char, options: u32, status: &mut UErrorCode, ) -> *mut UCaseMap; pub type ucasemap_utf8FoldCase = unsafe extern "C" fn( csm: *const UCaseMap, dest: *mut c_char, dest_capacity: i32, src: *const c_char, src_length: i32, status: &mut UErrorCode, ) -> i32; #[repr(C)] pub enum UCollationResult { UCOL_EQUAL = 0, UCOL_GREATER = 1, UCOL_LESS = -1, } #[repr(C)] pub struct UCollator; pub type ucol_open = unsafe extern "C" fn(loc: *const c_char, status: &mut UErrorCode) -> *mut UCollator; pub type ucol_setAttribute = unsafe extern "C" fn(coll: *mut UCollator, attr: i32, value: i32, status: &mut UErrorCode); pub const UCOL_NORMALIZATION_MODE: i32 = 4; pub const UCOL_NUMERIC_COLLATION: i32 = 7; pub const UCOL_ON: i32 = 17; pub type ucol_strcollUTF8 = unsafe extern "C" fn( coll: *mut UCollator, source: *const u8, source_length: i32, target: *const u8, target_length: i32, status: &mut UErrorCode, ) -> UCollationResult; // UText callback functions pub type UTextClone = unsafe extern "C" fn( dest: *mut UText, src: &UText, deep: bool, status: &mut UErrorCode, ) -> *mut UText; pub type UTextNativeLength = unsafe extern "C" fn(ut: &mut UText) -> i64; pub type UTextAccess = unsafe extern "C" fn(ut: &mut UText, native_index: i64, forward: bool) -> bool; pub type UTextExtract = unsafe extern "C" fn( ut: &mut UText, native_start: i64, native_limit: i64, dest: *mut u16, dest_capacity: i32, status: &mut UErrorCode, ) -> i32; pub type UTextReplace = unsafe extern "C" fn( ut: &mut UText, native_start: i64, native_limit: i64, replacement_text: *const u16, replacement_length: i32, status: &mut UErrorCode, ) -> i32; pub type UTextCopy = unsafe extern "C" fn( ut: &mut UText, native_start: i64, native_limit: i64, native_dest: i64, move_text: bool, status: &mut UErrorCode, ); pub type UTextMapOffsetToNative = unsafe extern "C" fn(ut: &UText) -> i64; pub type UTextMapNativeIndexToUTF16 = unsafe extern "C" fn(ut: &UText, native_index: i64) -> i32; pub type UTextClose = unsafe extern "C" fn(ut: &mut UText); #[repr(C)] pub struct UTextFuncs { pub table_size: i32, pub reserved1: i32, pub reserved2: i32, pub reserved3: i32, pub clone: Option, pub native_length: Option, pub access: Option, pub extract: Option, pub replace: Option, pub copy: Option, pub map_offset_to_native: Option, pub map_native_index_to_utf16: Option, pub close: Option, pub spare1: Option, pub spare2: Option, pub spare3: Option, } #[repr(C)] pub struct UText { pub magic: u32, pub flags: i32, pub provider_properties: i32, pub size_of_struct: i32, pub chunk_native_limit: i64, pub extra_size: i32, pub native_indexing_limit: i32, pub chunk_native_start: i64, pub chunk_offset: i32, pub chunk_length: i32, pub chunk_contents: *const u16, pub p_funcs: &'static UTextFuncs, pub p_extra: *mut c_void, pub context: *mut c_void, pub p: *mut c_void, pub q: *mut c_void, pub r: *mut c_void, pub priv_p: *mut c_void, pub a: i64, pub b: i32, pub c: i32, pub priv_a: i64, pub priv_b: i32, pub priv_c: i32, } pub const UTEXT_MAGIC: u32 = 0x345ad82c; pub const UTEXT_PROVIDER_LENGTH_IS_EXPENSIVE: i32 = 1; pub const UTEXT_PROVIDER_STABLE_CHUNKS: i32 = 2; pub const UTEXT_PROVIDER_WRITABLE: i32 = 3; pub const UTEXT_PROVIDER_HAS_META_DATA: i32 = 4; pub const UTEXT_PROVIDER_OWNS_TEXT: i32 = 5; pub type utext_setup = unsafe extern "C" fn( ut: *mut UText, extra_space: i32, status: &mut UErrorCode, ) -> *mut UText; pub type utext_close = unsafe extern "C" fn(ut: *mut UText) -> *mut UText; #[repr(C)] pub struct UParseError { pub line: i32, pub offset: i32, pub pre_context: [u16; 16], pub post_context: [u16; 16], } #[repr(C)] pub struct URegularExpression; pub const UREGEX_UNIX_LINES: i32 = 1; pub const UREGEX_CASE_INSENSITIVE: i32 = 2; pub const UREGEX_COMMENTS: i32 = 4; pub const UREGEX_MULTILINE: i32 = 8; pub const UREGEX_LITERAL: i32 = 16; pub const UREGEX_DOTALL: i32 = 32; pub const UREGEX_UWORD: i32 = 256; pub const UREGEX_ERROR_ON_UNKNOWN_ESCAPES: i32 = 512; pub type uregex_open = unsafe extern "C" fn( pattern: *const u16, pattern_length: i32, flags: i32, pe: Option<&mut UParseError>, status: &mut UErrorCode, ) -> *mut URegularExpression; pub type uregex_close = unsafe extern "C" fn(regexp: *mut URegularExpression); pub type uregex_setTimeLimit = unsafe extern "C" fn(regexp: *mut URegularExpression, limit: i32, status: &mut UErrorCode); pub type uregex_setUText = unsafe extern "C" fn( regexp: *mut URegularExpression, text: *mut UText, status: &mut UErrorCode, ); pub type uregex_reset64 = unsafe extern "C" fn(regexp: *mut URegularExpression, index: i64, status: &mut UErrorCode); pub type uregex_findNext = unsafe extern "C" fn(regexp: *mut URegularExpression, status: &mut UErrorCode) -> bool; pub type uregex_groupCount = unsafe extern "C" fn(regexp: *mut URegularExpression, status: &mut UErrorCode) -> i32; pub type uregex_start64 = unsafe extern "C" fn( regexp: *mut URegularExpression, group_num: i32, status: &mut UErrorCode, ) -> i64; pub type uregex_end64 = unsafe extern "C" fn( regexp: *mut URegularExpression, group_num: i32, status: &mut UErrorCode, ) -> i64; } #[cfg(test)] mod tests { use super::*; #[ignore] #[test] fn init() { assert!(init_if_needed().is_ok()); } #[test] fn test_compare_strings_ascii() { // Empty strings assert_eq!(compare_strings_ascii(b"", b""), Ordering::Equal); // Equal strings assert_eq!(compare_strings_ascii(b"hello", b"hello"), Ordering::Equal); // Different lengths assert_eq!(compare_strings_ascii(b"abc", b"abcd"), Ordering::Less); assert_eq!(compare_strings_ascii(b"abcd", b"abc"), Ordering::Greater); // Same chars, different cases - 1st char wins assert_eq!(compare_strings_ascii(b"AbC", b"aBc"), Ordering::Less); // Different chars, different cases - 2nd char wins, because it differs assert_eq!(compare_strings_ascii(b"hallo", b"Hello"), Ordering::Less); assert_eq!(compare_strings_ascii(b"Hello", b"hallo"), Ordering::Greater); } } ================================================ FILE: crates/edit/src/input.rs ================================================ // 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 std::mem; 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 awkward 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 { 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) -> Self { Self(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) -> Self { Self(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: Self) -> bool { (self.0 & modifier.0) != 0 } } impl std::ops::BitOr for InputKey { type Output = Self; fn bitor(self, rhs: InputKeyMod) -> Self { Self(self.0 | rhs.0) } } impl std::ops::BitOr 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); } /// 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(&'input str), /// A clipboard paste. Paste(Vec), /// Keyboard input. Keyboard(InputKey), /// Mouse input. Mouse(InputMouse), } /// Parses VT sequences into input events. pub struct Parser { bracketed_paste: bool, bracketed_paste_buf: Vec, x10_mouse_want: bool, x10_mouse_buf: [char; 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, bracketed_paste_buf: Vec::new(), 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. pub struct Stream<'parser, 'vt, 'input> { parser: &'parser mut Parser, stream: vt::Stream<'vt, 'input>, } impl<'input> Iterator for Stream<'_, '_, 'input> { type Item = Input<'input>; fn next(&mut self) -> Option> { loop { if self.parser.bracketed_paste { return self.handle_bracketed_paste(); } if self.parser.x10_mouse_want { return self.parse_x10_mouse_coordinates(); } const KEYPAD_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 ]; match self.stream.next()? { vt::Token::Text(text) => { return Some(Input::Text(text)); } 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) => match ch { 'A'..='H' => { let vk = KEYPAD_LUT[ch as usize - 'A' as usize]; if vk != 0 { return Some(Input::Keyboard(InputKey::new(vk as u32))); } } 'P'..='S' => { 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' => { let vk = KEYPAD_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 & 0x10) != 0 { kbmod::CTRL } else { kbmod::NONE }; mouse.position.x = csi.params[1] as CoordType - 1; mouse.position.y = csi.params[2] as CoordType - 1; 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 })); } _ => {} } } _ => {} } } } } impl<'input> Stream<'_, '_, 'input> { /// 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 /// [201~ lots of text [201~ /// ``` /// /// That in between text is then expected to be taken literally. /// It can be in between anything though, including other escape sequences. /// This is the reason why this is a separate method. #[cold] fn handle_bracketed_paste(&mut self) -> Option> { 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 { self.parser .bracketed_paste_buf .extend_from_slice(&self.stream.input().as_bytes()[beg..end]); } if !self.parser.bracketed_paste { Some(Input::Paste(mem::take(&mut self.parser.bracketed_paste_buf))) } 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> { while self.parser.x10_mouse_len < 3 && !self.stream.done() { self.parser.x10_mouse_buf[self.parser.x10_mouse_len] = self.stream.next_char(); self.parser.x10_mouse_len += 1; } if self.parser.x10_mouse_len < 3 { return None; } let b = self.parser.x10_mouse_buf[0] as u32; let x = self.parser.x10_mouse_buf[1] as CoordType - 0x21; let y = self.parser.x10_mouse_buf[2] as CoordType - 0x21; let action = match b & 0b11 { 0 => InputMouseState::Left, 1 => InputMouseState::Middle, 2 => InputMouseState::Right, _ => InputMouseState::None, }; let modifiers = { let mut m = kbmod::NONE; if (b & 0b00100) != 0 { m |= kbmod::SHIFT; } if (b & 0b01000) != 0 { m |= kbmod::ALT; } if (b & 0b10000) != 0 { m |= kbmod::CTRL; } m }; self.parser.x10_mouse_want = false; 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 } } ================================================ FILE: crates/edit/src/json.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! A simple JSONC parser with trailing comma support. //! //! It's designed for parsing our small settings files, //! but its performance is rather competitive in general. use std::fmt; use std::hint::unreachable_unchecked; use stdext::arena::Arena; use stdext::collections::{BString, BVec}; use crate::unicode::MeasurementConfig; /// Maximum nesting depth to prevent stack overflow. const MAX_DEPTH: usize = 64; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ParseErrorKind { /// Invalid JSON syntax Syntax, /// Maximum nesting depth exceeded MaxDepth, } #[derive(Debug, Clone)] pub struct ParseError { kind: ParseErrorKind, line: usize, column: usize, } impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let message = match self.kind { ParseErrorKind::Syntax => "Invalid JSON", ParseErrorKind::MaxDepth => "JSON too deeply nested", }; write!(f, "{}:{}: {}", self.line, self.column, message) } } impl std::error::Error for ParseError {} #[derive(Debug, Clone)] pub enum Value<'a> { Null, Bool(bool), Number(f64), String(&'a str), Array(&'a [Value<'a>]), Object(&'a [(&'a str, Value<'a>)]), } impl<'a> Value<'a> { pub fn is_null(&self) -> bool { matches!(self, Value::Null) } pub fn as_bool(&self) -> Option { match self { Value::Bool(b) => Some(*b), _ => None, } } pub fn as_number(&self) -> Option { match self { Value::Number(n) => Some(*n), _ => None, } } pub fn as_str(&self) -> Option<&'a str> { match self { Value::String(s) => Some(s), _ => None, } } pub fn as_array(&self) -> Option<&'a [Value<'a>]> { match self { Value::Array(arr) => Some(arr), _ => None, } } pub fn as_object(&self) -> Option> { match self { Value::Object(entries) => Some(Object { entries }), _ => None, } } } #[derive(Debug, Clone, Copy)] pub struct Object<'a> { entries: &'a [(&'a str, Value<'a>)], } impl<'a> Object<'a> { pub fn get(&self, key: &str) -> Option<&'a Value<'a>> { self.entries.iter().find(|e| e.0 == key).map(|e| &e.1) } pub fn get_bool(&self, key: &str) -> Option { self.get(key).and_then(Value::as_bool) } pub fn get_number(&self, key: &str) -> Option { self.get(key).and_then(Value::as_number) } pub fn get_str(&self, key: &str) -> Option<&'a str> { self.get(key).and_then(Value::as_str) } pub fn get_array(&self, key: &str) -> Option<&'a [Value<'a>]> { self.get(key).and_then(Value::as_array) } pub fn get_object(&self, key: &str) -> Option> { self.get(key).and_then(Value::as_object) } pub fn iter(&self) -> impl Iterator)> { self.entries.iter() } pub fn len(&self) -> usize { self.entries.len() } pub fn is_empty(&self) -> bool { self.entries.is_empty() } } pub fn parse<'a>(arena: &'a Arena, input: &str) -> Result, ParseError> { let mut parser = Parser::new(arena, input); parser.skip_bom(); let value = parser.parse_value(0)?; parser.skip_whitespace_and_comments()?; if parser.pos == parser.input.len() { Ok(value) } else { // Unexpected data after JSON value Err(parser.fail(parser.pos, ParseErrorKind::Syntax)) } } struct Parser<'a, 'i> { arena: &'a Arena, input: &'i str, bytes: &'i [u8], pos: usize, } impl<'a, 'i> Parser<'a, 'i> { fn new(arena: &'a Arena, input: &'i str) -> Self { Self { arena, input, bytes: input.as_bytes(), pos: 0 } } fn parse_value(&mut self, depth: usize) -> Result, ParseError> { // Prevent stack overflow from deeply nested structures if depth >= MAX_DEPTH { return Err(self.fail(self.pos, ParseErrorKind::MaxDepth)); } self.skip_whitespace_and_comments()?; let ch = match self.peek() { Some(ch) => ch, // Unexpected end of input None => return Err(self.fail(self.pos, ParseErrorKind::Syntax)), }; match ch { 'n' => self.parse_null(), 't' => self.parse_true(), 'f' => self.parse_false(), '-' | '0'..='9' => self.parse_number(), '"' => self.parse_string(), '[' => self.parse_array(depth), '{' => self.parse_object(depth), _ => Err(self.fail(self.pos, ParseErrorKind::Syntax)), } } fn parse_null(&mut self) -> Result, ParseError> { self.expect_str("null")?; Ok(Value::Null) } fn parse_true(&mut self) -> Result, ParseError> { self.expect_str("true")?; Ok(Value::Bool(true)) } fn parse_false(&mut self) -> Result, ParseError> { self.expect_str("false")?; Ok(Value::Bool(false)) } fn parse_number(&mut self) -> Result, ParseError> { let start = self.pos; while self.pos < self.bytes.len() && matches!(self.bytes[self.pos], b'0'..=b'9' | b'.' | b'-' | b'+' | b'e' | b'E') { self.pos += 1; } if let Ok(num) = self.input[start..self.pos].parse::() && num.is_finite() { Ok(Value::Number(num)) } else { Err(self.fail(self.pos, ParseErrorKind::Syntax)) } } fn parse_string(&mut self) -> Result, ParseError> { self.expect(b'"')?; let mut result = BString::empty(); loop { if self.pos >= self.bytes.len() { // Unterminated string return Err(self.fail(self.pos, ParseErrorKind::Syntax)); } let b = self.bytes[self.pos]; self.pos += 1; match b { b'"' => break, b'\\' => self.parse_escape(&mut result)?, ..=0x1f => { // Control characters must be escaped return Err(self.fail(self.pos - 1, ParseErrorKind::Syntax)); } _ => { let beg = self.pos - 1; while self.pos < self.bytes.len() && !matches!(self.bytes[self.pos], b'"' | b'\\' | ..=0x1f) { self.pos += 1; } result.push_str(self.arena, &self.input[beg..self.pos]); } } } let str = result.leak(); Ok(Value::String(str)) } #[cold] fn parse_escape(&mut self, result: &mut BString<'a>) -> Result<(), ParseError> { if self.pos >= self.bytes.len() { // Unterminated escape sequence return Err(self.fail(self.pos, ParseErrorKind::Syntax)); } let b = self.bytes[self.pos]; self.pos += 1; let ch = match b { b'"' => b'"', b'\\' => b'\\', b'/' => b'/', b'b' => b'\x08', b'f' => b'\x0C', b'n' => b'\n', b'r' => b'\r', b't' => b'\t', b'u' => return self.parse_unicode_escape(result), _ => { // Invalid escape sequence return Err(self.fail(self.pos - 2, ParseErrorKind::Syntax)); } }; result.push(self.arena, ch as char); Ok(()) } #[cold] fn parse_unicode_escape(&mut self, result: &mut BString<'a>) -> Result<(), ParseError> { let start = self.pos - 2; // parse_escape() already advanced past "\u" let mut code = self.parse_hex4()?; if (0xd800..=0xdbff).contains(&code) { if self.is_str("\\u") && let _ = self.advance(2) && let Ok(low) = self.parse_hex4() && (0xdc00..=0xdfff).contains(&low) { code = 0x10000 + ((code - 0xd800) << 10) + (low - 0xdc00); } else { code = u32::MAX; }; } match char::from_u32(code) { Some(c) => { result.push(self.arena, c); Ok(()) } None => Err(self.fail(start, ParseErrorKind::Syntax)), } } fn parse_hex4(&mut self) -> Result { let start = self.pos - 2; // parse_unicode_escape() already advanced past "\u" self.bytes .get(self.pos..self.pos + 4) .and_then(|b| { self.pos += 4; b.iter().try_fold(0u32, |acc, &b| { let d = (b as char).to_digit(16)?; Some((acc << 4) | d) }) }) .ok_or_else(|| self.fail(start, ParseErrorKind::Syntax)) } fn parse_array(&mut self, depth: usize) -> Result, ParseError> { let mut values = BVec::empty(); let mut expects_comma = false; self.expect(b'[')?; loop { self.skip_whitespace_and_comments()?; match self.peek() { // Unexpected end of input None => return Err(self.fail(self.pos, ParseErrorKind::Syntax)), Some(']') => break, Some(',') => { if !expects_comma { // Unexpected comma return Err(self.fail(self.pos, ParseErrorKind::Syntax)); } self.advance(1); self.skip_whitespace_and_comments()?; expects_comma = false; } Some(_) => { if expects_comma { // Missing comma return Err(self.fail(self.pos, ParseErrorKind::Syntax)); } values.push(self.arena, self.parse_value(depth + 1)?); expects_comma = true; } } } self.expect(b']')?; Ok(Value::Array(values.leak())) } fn parse_object(&mut self, depth: usize) -> Result, ParseError> { let mut entries = BVec::empty(); let mut expects_comma = false; self.expect(b'{')?; loop { self.skip_whitespace_and_comments()?; match self.peek() { // Unexpected end of input None => return Err(self.fail(self.pos, ParseErrorKind::Syntax)), Some(',') => { if !expects_comma { // Unexpected comma return Err(self.fail(self.pos, ParseErrorKind::Syntax)); } self.advance(1); self.skip_whitespace_and_comments()?; expects_comma = false; } Some('}') => break, Some(_) => { if expects_comma { // Missing comma return Err(self.fail(self.pos, ParseErrorKind::Syntax)); } let key = match self.parse_string()? { Value::String(s) => s, // The entire point of parse_string is to return a string. // If that fails, we all should start farming potatoes. // This is essentially an unwrap_unchecked(). _ => unsafe { unreachable_unchecked() }, }; self.skip_whitespace_and_comments()?; self.expect(b':')?; let value = self.parse_value(depth + 1)?; entries.push(self.arena, (key, value)); expects_comma = true; } } } self.expect(b'}')?; Ok(Value::Object(entries.leak())) } fn skip_bom(&mut self) { if self.is_str("\u{feff}") { self.advance(3); } } fn skip_whitespace_and_comments(&mut self) -> Result<(), ParseError> { loop { loop { if self.pos >= self.bytes.len() { return Ok(()); } match self.bytes[self.pos] { b' ' | b'\t' | b'\n' | b'\r' => self.pos += 1, _ => break, } } if self.is_str("//") { self.pos += 2; while self.pos < self.bytes.len() && self.bytes[self.pos] != b'\n' { self.pos += 1; } } else if self.is_str("/*") { let start = self.pos; self.pos += 2; loop { while self.pos < self.bytes.len() && self.bytes[self.pos] != b'*' { self.pos += 1; } if self.pos >= self.bytes.len() { return Err(self.fail(start, ParseErrorKind::Syntax)); } if self.is_str("*/") { self.pos += 2; break; } self.pos += 1; } } else { return Ok(()); } } } fn expect(&mut self, expected: u8) -> Result<(), ParseError> { if self.bytes.get(self.pos) == Some(&expected) { self.pos += 1; Ok(()) } else { Err(self.fail(self.pos, ParseErrorKind::Syntax)) } } fn expect_str(&mut self, expected: &str) -> Result<(), ParseError> { if self.is_str(expected) { self.pos += expected.len(); Ok(()) } else { Err(self.fail(self.pos, ParseErrorKind::Syntax)) } } fn is_str(&self, expected: &str) -> bool { self.bytes.get(self.pos..self.pos + expected.len()) == Some(expected.as_bytes()) } fn peek(&self) -> Option { if self.pos < self.bytes.len() { Some(self.bytes[self.pos] as char) } else { None } } fn advance(&mut self, num: usize) { self.pos += num; } #[cold] fn fail(&self, pos: usize, kind: ParseErrorKind) -> ParseError { let mut cfg = MeasurementConfig::new(&self.bytes); let pos = cfg.goto_offset(pos); let line = pos.logical_pos.y.max(0) as usize + 1; let column = pos.logical_pos.x.max(0) as usize + 1; ParseError { kind, line, column } } } #[allow(non_snake_case)] #[allow(clippy::invisible_characters)] #[cfg(test)] mod tests { use stdext::arena::scratch_arena; use super::*; #[test] fn test_null() { let scratch = scratch_arena(None); assert!(parse(&scratch, "null").unwrap().is_null()); } #[test] fn test_bool() { let scratch = scratch_arena(None); assert_eq!(parse(&scratch, "true").unwrap().as_bool(), Some(true)); assert_eq!(parse(&scratch, "false").unwrap().as_bool(), Some(false)); } #[test] fn test_number() { let scratch = scratch_arena(None); assert_eq!(parse(&scratch, "0").unwrap().as_number(), Some(0.0)); assert_eq!(parse(&scratch, "123").unwrap().as_number(), Some(123.0)); assert_eq!(parse(&scratch, "-456").unwrap().as_number(), Some(-456.0)); assert_eq!(parse(&scratch, "3.15").unwrap().as_number(), Some(3.15)); assert_eq!(parse(&scratch, "1e10").unwrap().as_number(), Some(1e10)); assert_eq!(parse(&scratch, "1.5e-3").unwrap().as_number(), Some(0.0015)); } #[test] fn test_string() { let scratch = scratch_arena(None); assert_eq!(parse(&scratch, r#""hello""#).unwrap().as_str(), Some("hello")); assert_eq!(parse(&scratch, r#""hello\nworld""#).unwrap().as_str(), Some("hello\nworld")); assert_eq!(parse(&scratch, r#""\u0041\u0042\u0043""#).unwrap().as_str(), Some("ABC")); } #[test] fn test_array() { let scratch = scratch_arena(None); let value = parse(&scratch, "[1, 2, 3]").unwrap(); let arr = value.as_array().unwrap(); assert_eq!(arr.len(), 3); assert_eq!(arr[0].as_number(), Some(1.0)); assert_eq!(arr[1].as_number(), Some(2.0)); assert_eq!(arr[2].as_number(), Some(3.0)); } #[test] fn test_object() { let scratch = scratch_arena(None); let value = parse(&scratch, r#"{"a": 1, "b": true}"#).unwrap(); let obj = value.as_object().unwrap(); assert_eq!(obj.get_number("a"), Some(1.0)); assert_eq!(obj.get_bool("b"), Some(true)); } #[test] fn test_comments() { let scratch = scratch_arena(None); let input = r#"{ // Line comment "a": 1, /* Block comment */ "b": 2 }"#; let value = parse(&scratch, input).unwrap(); let obj = value.as_object().unwrap(); assert_eq!(obj.get_number("a"), Some(1.0)); assert_eq!(obj.get_number("b"), Some(2.0)); } #[test] fn test_trailing_comma() { let scratch = scratch_arena(None); assert!(parse(&scratch, "[1, 2, 3,]").is_ok()); assert!(parse(&scratch, r#"{"a": 1,}"#).is_ok()); } #[test] fn test_nested() { let scratch = scratch_arena(None); let input = r#"{ "nested": { "array": [1, 2, {"deep": true}] } }"#; let value = parse(&scratch, input).unwrap(); let obj = value.as_object().unwrap(); let nested = obj.get_object("nested").unwrap(); let array = nested.get_array("array").unwrap(); assert_eq!(array.len(), 3); let deep_obj = array[2].as_object().unwrap(); assert_eq!(deep_obj.get_bool("deep"), Some(true)); } #[test] fn test_max_depth() { let scratch = scratch_arena(None); let mut input = String::new(); for _ in 0..100 { input.push('['); } for _ in 0..100 { input.push(']'); } assert!(parse(&scratch, &input).is_err()); } #[test] fn test_invalid_json() { let scratch = scratch_arena(None); assert!(parse(&scratch, "").is_err()); assert!(parse(&scratch, "{").is_err()); assert!(parse(&scratch, r#"{"a":}"#).is_err()); assert!(parse(&scratch, r#"{5:1}"#).is_err()); assert!(parse(&scratch, "[1, 2,").is_err()); assert!(parse(&scratch, r#""unterminated"#).is_err()); } #[test] fn test_control_chars() { let scratch = scratch_arena(None); // Control characters must be escaped assert!(parse(&scratch, "\"\x01\"").is_err()); } #[test] fn test_unicode() { let scratch = scratch_arena(None); // Test emoji (surrogate pair) assert_eq!(parse(&scratch, r#""\uD83D\uDE00""#).unwrap().as_str(), Some("😀")); // Test regular unicode assert_eq!(parse(&scratch, r#""\u2764""#).unwrap().as_str(), Some("❤")); } } ================================================ FILE: crates/edit/src/lib.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #![cfg_attr( target_arch = "loongarch64", feature(stdarch_loongarch, stdarch_loongarch_feature_detection, loongarch_target_feature), allow(clippy::incompatible_msrv) )] #![allow(clippy::missing_transmute_annotations, clippy::new_without_default, stable_features)] pub mod base64; pub mod buffer; pub mod cell; pub mod clipboard; pub mod document; pub mod framebuffer; pub mod fuzzy; pub mod glob; pub mod hash; pub mod helpers; pub mod icu; pub mod input; pub mod json; pub mod oklab; pub mod path; pub mod simd; pub mod sys; pub mod tui; pub mod unicode; pub mod vt; ================================================ FILE: crates/edit/src/oklab.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! Oklab colorspace conversions. //! //! Implements Oklab as defined at: #![allow(clippy::excessive_precision)] use std::fmt::Debug; /// A sRGB color with straight (= not premultiplied) alpha. #[derive(Default, Clone, Copy, PartialEq, Eq)] #[repr(transparent)] pub struct StraightRgba(u32); impl StraightRgba { #[inline] pub const fn zero() -> Self { StraightRgba(0) } #[inline] pub const fn from_le(color: u32) -> Self { StraightRgba(u32::from_le(color)) } #[inline] pub const fn from_be(color: u32) -> Self { StraightRgba(u32::from_be(color)) } #[inline] pub const fn to_ne(self) -> u32 { self.0 } #[inline] pub const fn to_le(self) -> u32 { self.0.to_le() } #[inline] pub const fn to_be(self) -> u32 { self.0.to_be() } #[inline] pub const fn red(self) -> u32 { self.0 & 0xff } #[inline] pub const fn green(self) -> u32 { (self.0 >> 8) & 0xff } #[inline] pub const fn blue(self) -> u32 { (self.0 >> 16) & 0xff } #[inline] pub const fn alpha(self) -> u32 { self.0 >> 24 } pub fn oklab_blend(self, top: StraightRgba) -> StraightRgba { let bottom = self.as_oklab(); let top = top.as_oklab(); let result = bottom.blend(&top); result.as_rgba() } pub fn as_oklab(self) -> Oklab { let r = srgb_to_linear(self.red()); let g = srgb_to_linear(self.green()); let b = srgb_to_linear(self.blue()); let alpha = self.alpha() as f32 * (1.0 / 255.0); let l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b; let m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b; let s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b; let l_ = cbrtf_est(l); let m_ = cbrtf_est(m); let s_ = cbrtf_est(s); let l = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_; let a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_; let b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_; Oklab([l, a, b, alpha]) } } impl Debug for StraightRgba { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "#{:08x}", self.0.to_be()) // Display as a hex color } } /// An Oklab color with alpha. By convention, it uses straight alpha. #[derive(Clone, Copy)] pub struct Oklab([f32; 4]); impl Oklab { #[inline] pub const fn lightness(self) -> f32 { self.0[0] } #[inline] pub const fn a(self) -> f32 { self.0[1] } #[inline] pub const fn b(self) -> f32 { self.0[2] } #[inline] pub const fn alpha(self) -> f32 { self.0[3] } pub fn as_rgba(&self) -> StraightRgba { let l_ = self.lightness() + 0.3963377774 * self.a() + 0.2158037573 * self.b(); let m_ = self.lightness() - 0.1055613458 * self.a() - 0.0638541728 * self.b(); let s_ = self.lightness() - 0.0894841775 * self.a() - 1.2914855480 * self.b(); let l = l_ * l_ * l_; let m = m_ * m_ * m_; let s = s_ * s_ * s_; let r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s; let g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s; let b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s; let r = r.clamp(0.0, 1.0); let g = g.clamp(0.0, 1.0); let b = b.clamp(0.0, 1.0); let alpha = self.alpha().clamp(0.0, 1.0); let r = linear_to_srgb(r); let g = linear_to_srgb(g); let b = linear_to_srgb(b); let a = (alpha * 255.0) as u32; StraightRgba(r | (g << 8) | (b << 16) | (a << 24)) } /// Porter-Duff "over" composition. It's for Lab, but it works just like with RGB. /// The benefit of the Oklab colorspace is its perceptual uniformity, which RGB lacks. /// This can be observed easily when blending red and green for instance. pub fn blend(&self, top: &Self) -> Self { let top_a = top.alpha(); let bottom_a = self.alpha() * (1.0 - top_a); let l = top.lightness() * top_a + self.lightness() * bottom_a; let a = top.a() * top_a + self.a() * bottom_a; let b = top.b() * top_a + self.b() * bottom_a; let alpha = top_a + bottom_a; let inv_alpha = if alpha > 0.0 { 1.0 / alpha } else { 0.0 }; let l = l * inv_alpha; let a = a * inv_alpha; let b = b * inv_alpha; Self([l, a, b, alpha]) } } fn srgb_to_linear(c: u32) -> f32 { SRGB_TO_RGB_LUT[(c & 0xff) as usize] } fn linear_to_srgb(c: f32) -> u32 { (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, ]; #[cfg(test)] mod tests { use super::*; #[test] fn test_blending() { let lower = StraightRgba::from_be(0x3498dbff); let upper = StraightRgba::from_be(0xe74c3c7f); let expected = StraightRgba::from_be(0xa67f93ff); let blended = lower.oklab_blend(upper); assert_eq!(blended, expected); } } ================================================ FILE: crates/edit/src/path.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! Path related helpers. use std::ffi::{OsStr, OsString}; 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 { let mut res = PathBuf::with_capacity(path.as_os_str().as_encoded_bytes().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().as_encoded_bytes().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 { // Pop the last component from `res`. // // This can be replaced with a plain `res.as_mut_os_string().truncate(len)` // once `os_string_truncate` is stabilized (#133262). let mut bytes = res.into_os_string().into_encoded_bytes(); bytes.truncate(len); res = PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(bytes) }); } } 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"); } } ================================================ FILE: crates/edit/src/simd/lines_bwd.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use std::ptr; use crate::helpers::CoordType; /// Starting from the `offset` in `haystack` with a current line index of /// `line`, this seeks backwards to the `line_stop`-nth line and returns the /// new offset and the line index at that point. /// /// Note that this function differs from `lines_fwd` in that it /// seeks backwards even if the `line` is already at `line_stop`. /// This allows you to ensure (or test) whether `offset` is at a line start. /// /// It returns an offset *past* a newline and thus at the start of a line. pub fn lines_bwd( haystack: &[u8], offset: usize, line: CoordType, line_stop: CoordType, ) -> (usize, CoordType) { unsafe { let beg = haystack.as_ptr(); let it = beg.add(offset.min(haystack.len())); let (it, line) = lines_bwd_raw(beg, it, line, line_stop); (it.offset_from_unsigned(beg), line) } } unsafe fn lines_bwd_raw( beg: *const u8, end: *const u8, line: CoordType, line_stop: CoordType, ) -> (*const u8, CoordType) { #[cfg(any(target_arch = "x86_64", target_arch = "loongarch64"))] return unsafe { LINES_BWD_DISPATCH(beg, end, line, line_stop) }; #[cfg(target_arch = "aarch64")] return unsafe { lines_bwd_neon(beg, end, line, line_stop) }; #[allow(unreachable_code)] return unsafe { lines_bwd_fallback(beg, end, line, line_stop) }; } unsafe fn lines_bwd_fallback( beg: *const u8, mut end: *const u8, mut line: CoordType, line_stop: CoordType, ) -> (*const u8, CoordType) { unsafe { while !ptr::eq(end, beg) { let n = end.sub(1); if *n == b'\n' { if line <= line_stop { break; } line -= 1; } end = n; } (end, line) } } #[cfg(any(target_arch = "x86_64", target_arch = "loongarch64"))] static mut LINES_BWD_DISPATCH: unsafe fn( beg: *const u8, end: *const u8, line: CoordType, line_stop: CoordType, ) -> (*const u8, CoordType) = lines_bwd_dispatch; #[cfg(target_arch = "x86_64")] unsafe fn lines_bwd_dispatch( beg: *const u8, end: *const u8, line: CoordType, line_stop: CoordType, ) -> (*const u8, CoordType) { let func = if is_x86_feature_detected!("avx2") { lines_bwd_avx2 } else { lines_bwd_fallback }; unsafe { LINES_BWD_DISPATCH = func }; unsafe { func(beg, end, line, line_stop) } } #[cfg(target_arch = "x86_64")] #[target_feature(enable = "avx2")] unsafe fn lines_bwd_avx2( beg: *const u8, mut end: *const u8, mut line: CoordType, line_stop: CoordType, ) -> (*const u8, CoordType) { unsafe { use std::arch::x86_64::*; #[inline(always)] unsafe fn horizontal_sum_i64(v: __m256i) -> i64 { unsafe { let hi = _mm256_extracti128_si256::<1>(v); let lo = _mm256_castsi256_si128(v); let sum = _mm_add_epi64(lo, hi); let shuf = _mm_shuffle_epi32::<0b11_10_11_10>(sum); let sum = _mm_add_epi64(sum, shuf); _mm_cvtsi128_si64(sum) } } let lf = _mm256_set1_epi8(b'\n' as i8); let off = end.addr() & 31; if off != 0 && off < end.offset_from_unsigned(beg) { (end, line) = lines_bwd_fallback(end.sub(off), end, line, line_stop); } while end.offset_from_unsigned(beg) >= 128 { let chunk_start = end.sub(128); let v1 = _mm256_loadu_si256(chunk_start.add(0) as *const _); let v2 = _mm256_loadu_si256(chunk_start.add(32) as *const _); let v3 = _mm256_loadu_si256(chunk_start.add(64) as *const _); let v4 = _mm256_loadu_si256(chunk_start.add(96) as *const _); let mut sum = _mm256_setzero_si256(); sum = _mm256_sub_epi8(sum, _mm256_cmpeq_epi8(v1, lf)); sum = _mm256_sub_epi8(sum, _mm256_cmpeq_epi8(v2, lf)); sum = _mm256_sub_epi8(sum, _mm256_cmpeq_epi8(v3, lf)); sum = _mm256_sub_epi8(sum, _mm256_cmpeq_epi8(v4, lf)); let sum = _mm256_sad_epu8(sum, _mm256_setzero_si256()); let sum = horizontal_sum_i64(sum); let line_next = line - sum as CoordType; if line_next <= line_stop { break; } end = chunk_start; line = line_next; } while end.offset_from_unsigned(beg) >= 32 { let chunk_start = end.sub(32); let v = _mm256_loadu_si256(chunk_start as *const _); let c = _mm256_cmpeq_epi8(v, lf); let ones = _mm256_and_si256(c, _mm256_set1_epi8(0x01)); let sum = _mm256_sad_epu8(ones, _mm256_setzero_si256()); let sum = horizontal_sum_i64(sum); let line_next = line - sum as CoordType; if line_next <= line_stop { break; } end = chunk_start; line = line_next; } lines_bwd_fallback(beg, end, line, line_stop) } } #[cfg(target_arch = "loongarch64")] unsafe fn lines_bwd_dispatch( beg: *const u8, end: *const u8, line: CoordType, line_stop: CoordType, ) -> (*const u8, CoordType) { use std::arch::is_loongarch_feature_detected; let func = if is_loongarch_feature_detected!("lasx") { lines_bwd_lasx } else if is_loongarch_feature_detected!("lsx") { lines_bwd_lsx } else { lines_bwd_fallback }; unsafe { LINES_BWD_DISPATCH = func }; unsafe { func(beg, end, line, line_stop) } } #[cfg(target_arch = "loongarch64")] #[target_feature(enable = "lasx")] unsafe fn lines_bwd_lasx( beg: *const u8, mut end: *const u8, mut line: CoordType, line_stop: CoordType, ) -> (*const u8, CoordType) { unsafe { use std::arch::loongarch64::*; #[inline(always)] unsafe fn horizontal_sum(sum: m256i) -> u32 { unsafe { let sum = lasx_xvhaddw_h_b(sum, sum); let sum = lasx_xvhaddw_w_h(sum, sum); let sum = lasx_xvhaddw_d_w(sum, sum); let sum = lasx_xvhaddw_q_d(sum, sum); let tmp = lasx_xvpermi_q::<1>(sum, sum); let sum = lasx_xvadd_w(sum, tmp); lasx_xvpickve2gr_wu::<0>(sum) } } let lf = lasx_xvrepli_b(b'\n' as i32); let line_stop = line_stop.min(line); let off = end.addr() & 31; if off != 0 && off < end.offset_from_unsigned(beg) { (end, line) = lines_bwd_fallback(end.sub(off), end, line, line_stop); } while end.offset_from_unsigned(beg) >= 128 { let chunk_start = end.sub(128); let v1 = lasx_xvld::<0>(chunk_start as *const _); let v2 = lasx_xvld::<32>(chunk_start as *const _); let v3 = lasx_xvld::<64>(chunk_start as *const _); let v4 = lasx_xvld::<96>(chunk_start as *const _); let mut sum = lasx_xvrepli_b(0); sum = lasx_xvsub_b(sum, lasx_xvseq_b(v1, lf)); sum = lasx_xvsub_b(sum, lasx_xvseq_b(v2, lf)); sum = lasx_xvsub_b(sum, lasx_xvseq_b(v3, lf)); sum = lasx_xvsub_b(sum, lasx_xvseq_b(v4, lf)); let sum = horizontal_sum(sum); let line_next = line - sum as CoordType; if line_next <= line_stop { break; } end = chunk_start; line = line_next; } while end.offset_from_unsigned(beg) >= 32 { let chunk_start = end.sub(32); let v = lasx_xvld::<0>(chunk_start as *const _); let c = lasx_xvseq_b(v, lf); let ones = lasx_xvand_v(c, lasx_xvrepli_b(1)); let sum = horizontal_sum(ones); let line_next = line - sum as CoordType; if line_next <= line_stop { break; } end = chunk_start; line = line_next; } lines_bwd_fallback(beg, end, line, line_stop) } } #[cfg(target_arch = "loongarch64")] #[target_feature(enable = "lsx")] unsafe fn lines_bwd_lsx( beg: *const u8, mut end: *const u8, mut line: CoordType, line_stop: CoordType, ) -> (*const u8, CoordType) { unsafe { use std::arch::loongarch64::*; #[inline(always)] unsafe fn horizontal_sum(sum: m128i) -> u32 { unsafe { let sum = lsx_vhaddw_h_b(sum, sum); let sum = lsx_vhaddw_w_h(sum, sum); let sum = lsx_vhaddw_d_w(sum, sum); let sum = lsx_vhaddw_q_d(sum, sum); lsx_vpickve2gr_wu::<0>(sum) } } const LF: i32 = b'\n' as i32; let line_stop = line_stop.min(line); let off = end.addr() & 15; if off != 0 && off < end.offset_from_unsigned(beg) { (end, line) = lines_bwd_fallback(end.sub(off), end, line, line_stop); } while end.offset_from_unsigned(beg) >= 64 { let chunk_start = end.sub(64); let v1 = lsx_vld::<0>(chunk_start as *const _); let v2 = lsx_vld::<16>(chunk_start as *const _); let v3 = lsx_vld::<32>(chunk_start as *const _); let v4 = lsx_vld::<48>(chunk_start as *const _); let mut sum = lsx_vldi::<0>(); sum = lsx_vsub_b(sum, lsx_vseqi_b::(v1)); sum = lsx_vsub_b(sum, lsx_vseqi_b::(v2)); sum = lsx_vsub_b(sum, lsx_vseqi_b::(v3)); sum = lsx_vsub_b(sum, lsx_vseqi_b::(v4)); let sum = horizontal_sum(sum); let line_next = line - sum as CoordType; if line_next <= line_stop { break; } end = chunk_start; line = line_next; } while end.offset_from_unsigned(beg) >= 16 { let chunk_start = end.sub(16); let v = lsx_vld::<0>(chunk_start as *const _); let c = lsx_vseqi_b::(v); let ones = lsx_vandi_b::<1>(c); let sum = horizontal_sum(ones); let line_next = line - sum as CoordType; if line_next <= line_stop { break; } end = chunk_start; line = line_next; } lines_bwd_fallback(beg, end, line, line_stop) } } #[cfg(target_arch = "aarch64")] unsafe fn lines_bwd_neon( beg: *const u8, mut end: *const u8, mut line: CoordType, line_stop: CoordType, ) -> (*const u8, CoordType) { unsafe { use std::arch::aarch64::*; let lf = vdupq_n_u8(b'\n'); let line_stop = line_stop.min(line); let off = end.addr() & 15; if off != 0 && off < end.offset_from_unsigned(beg) { (end, line) = lines_bwd_fallback(end.sub(off), end, line, line_stop); } while end.offset_from_unsigned(beg) >= 64 { let chunk_start = end.sub(64); let v1 = vld1q_u8(chunk_start.add(0)); let v2 = vld1q_u8(chunk_start.add(16)); let v3 = vld1q_u8(chunk_start.add(32)); let v4 = vld1q_u8(chunk_start.add(48)); let mut sum = vdupq_n_u8(0); sum = vsubq_u8(sum, vceqq_u8(v1, lf)); sum = vsubq_u8(sum, vceqq_u8(v2, lf)); sum = vsubq_u8(sum, vceqq_u8(v3, lf)); sum = vsubq_u8(sum, vceqq_u8(v4, lf)); let sum = vaddvq_u8(sum); let line_next = line - sum as CoordType; if line_next <= line_stop { break; } end = chunk_start; line = line_next; } while end.offset_from_unsigned(beg) >= 16 { let chunk_start = end.sub(16); let v = vld1q_u8(chunk_start); let c = vceqq_u8(v, lf); let c = vandq_u8(c, vdupq_n_u8(0x01)); let sum = vaddvq_u8(c); let line_next = line - sum as CoordType; if line_next <= line_stop { break; } end = chunk_start; line = line_next; } lines_bwd_fallback(beg, end, line, line_stop) } } #[cfg(test)] mod test { use super::*; use crate::helpers::CoordType; use crate::simd::test::*; #[test] fn pseudo_fuzz() { let text = generate_random_text(1024); let lines = count_lines(&text); let mut offset_rng = make_rng(); let mut line_rng = make_rng(); let mut line_distance_rng = make_rng(); for _ in 0..1000 { let offset = offset_rng() % (text.len() + 1); let line_stop = line_distance_rng() % (lines + 1); let line = (line_stop + line_rng() % 100).saturating_sub(5); let line = line as CoordType; let line_stop = line_stop as CoordType; let expected = reference_lines_bwd(text.as_bytes(), offset, line, line_stop); let actual = lines_bwd(text.as_bytes(), offset, line, line_stop); assert_eq!(expected, actual); } } fn reference_lines_bwd( haystack: &[u8], mut offset: usize, mut line: CoordType, line_stop: CoordType, ) -> (usize, CoordType) { while offset > 0 { let c = haystack[offset - 1]; if c == b'\n' { if line <= line_stop { break; } line -= 1; } offset -= 1; } (offset, line) } #[test] fn seeks_to_start() { for i in 6..=11 { let (off, line) = lines_bwd(b"Hello\nWorld\n", i, 123, 456); assert_eq!(off, 6); // After "Hello\n" assert_eq!(line, 123); // Still on the same line } } } ================================================ FILE: crates/edit/src/simd/lines_fwd.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use std::ptr; use crate::helpers::CoordType; /// Starting from the `offset` in `haystack` with a current line index of /// `line`, this seeks to the `line_stop`-nth line and returns the /// new offset and the line index at that point. /// /// It returns an offset *past* the newline. /// If `line` is already at or past `line_stop`, it returns immediately. pub fn lines_fwd( haystack: &[u8], offset: usize, line: CoordType, line_stop: CoordType, ) -> (usize, CoordType) { unsafe { let beg = haystack.as_ptr(); let end = beg.add(haystack.len()); let it = beg.add(offset.min(haystack.len())); let (it, line) = lines_fwd_raw(it, end, line, line_stop); (it.offset_from_unsigned(beg), line) } } unsafe fn lines_fwd_raw( beg: *const u8, end: *const u8, line: CoordType, line_stop: CoordType, ) -> (*const u8, CoordType) { #[cfg(any(target_arch = "x86_64", target_arch = "loongarch64"))] return unsafe { LINES_FWD_DISPATCH(beg, end, line, line_stop) }; #[cfg(target_arch = "aarch64")] return unsafe { lines_fwd_neon(beg, end, line, line_stop) }; #[allow(unreachable_code)] return unsafe { lines_fwd_fallback(beg, end, line, line_stop) }; } unsafe fn lines_fwd_fallback( mut beg: *const u8, end: *const u8, mut line: CoordType, line_stop: CoordType, ) -> (*const u8, CoordType) { unsafe { if line < line_stop { while !ptr::eq(beg, end) { let c = *beg; beg = beg.add(1); if c == b'\n' { line += 1; if line == line_stop { break; } } } } (beg, line) } } #[cfg(any(target_arch = "x86_64", target_arch = "loongarch64"))] static mut LINES_FWD_DISPATCH: unsafe fn( beg: *const u8, end: *const u8, line: CoordType, line_stop: CoordType, ) -> (*const u8, CoordType) = lines_fwd_dispatch; #[cfg(target_arch = "x86_64")] unsafe fn lines_fwd_dispatch( beg: *const u8, end: *const u8, line: CoordType, line_stop: CoordType, ) -> (*const u8, CoordType) { let func = if is_x86_feature_detected!("avx2") { lines_fwd_avx2 } else { lines_fwd_fallback }; unsafe { LINES_FWD_DISPATCH = func }; unsafe { func(beg, end, line, line_stop) } } #[cfg(target_arch = "x86_64")] #[target_feature(enable = "avx2")] unsafe fn lines_fwd_avx2( mut beg: *const u8, end: *const u8, mut line: CoordType, line_stop: CoordType, ) -> (*const u8, CoordType) { unsafe { use std::arch::x86_64::*; #[inline(always)] unsafe fn horizontal_sum_i64(v: __m256i) -> i64 { unsafe { let hi = _mm256_extracti128_si256::<1>(v); let lo = _mm256_castsi256_si128(v); let sum = _mm_add_epi64(lo, hi); let shuf = _mm_shuffle_epi32::<0b11_10_11_10>(sum); let sum = _mm_add_epi64(sum, shuf); _mm_cvtsi128_si64(sum) } } let lf = _mm256_set1_epi8(b'\n' as i8); let off = beg.align_offset(32); if off != 0 && off < end.offset_from_unsigned(beg) { (beg, line) = lines_fwd_fallback(beg, beg.add(off), line, line_stop); } if line < line_stop { // Unrolling the loop by 4x speeds things up by >3x. // It allows us to accumulate matches before doing a single `vpsadbw`. while end.offset_from_unsigned(beg) >= 128 { let v1 = _mm256_loadu_si256(beg.add(0) as *const _); let v2 = _mm256_loadu_si256(beg.add(32) as *const _); let v3 = _mm256_loadu_si256(beg.add(64) as *const _); let v4 = _mm256_loadu_si256(beg.add(96) as *const _); // `vpcmpeqb` leaves each comparison result byte as 0 or -1 (0xff). // This allows us to accumulate the comparisons by subtracting them. let mut sum = _mm256_setzero_si256(); sum = _mm256_sub_epi8(sum, _mm256_cmpeq_epi8(v1, lf)); sum = _mm256_sub_epi8(sum, _mm256_cmpeq_epi8(v2, lf)); sum = _mm256_sub_epi8(sum, _mm256_cmpeq_epi8(v3, lf)); sum = _mm256_sub_epi8(sum, _mm256_cmpeq_epi8(v4, lf)); // Calculate the total number of matches in this chunk. let sum = _mm256_sad_epu8(sum, _mm256_setzero_si256()); let sum = horizontal_sum_i64(sum); let line_next = line + sum as CoordType; if line_next >= line_stop { break; } beg = beg.add(128); line = line_next; } while end.offset_from_unsigned(beg) >= 32 { let v = _mm256_loadu_si256(beg as *const _); let c = _mm256_cmpeq_epi8(v, lf); // If you ask an LLM, the best way to do this is // to do a `vpmovmskb` followed by `popcnt`. // One contemporary hardware that's a bad idea though. let ones = _mm256_and_si256(c, _mm256_set1_epi8(0x01)); let sum = _mm256_sad_epu8(ones, _mm256_setzero_si256()); let sum = horizontal_sum_i64(sum); let line_next = line + sum as CoordType; if line_next >= line_stop { break; } beg = beg.add(32); line = line_next; } } lines_fwd_fallback(beg, end, line, line_stop) } } #[cfg(target_arch = "loongarch64")] unsafe fn lines_fwd_dispatch( beg: *const u8, end: *const u8, line: CoordType, line_stop: CoordType, ) -> (*const u8, CoordType) { use std::arch::is_loongarch_feature_detected; let func = if is_loongarch_feature_detected!("lasx") { lines_fwd_lasx } else if is_loongarch_feature_detected!("lsx") { lines_fwd_lsx } else { lines_fwd_fallback }; unsafe { LINES_FWD_DISPATCH = func }; unsafe { func(beg, end, line, line_stop) } } #[cfg(target_arch = "loongarch64")] #[target_feature(enable = "lasx")] unsafe fn lines_fwd_lasx( mut beg: *const u8, end: *const u8, mut line: CoordType, line_stop: CoordType, ) -> (*const u8, CoordType) { unsafe { use std::arch::loongarch64::*; #[inline(always)] unsafe fn horizontal_sum(sum: m256i) -> u32 { unsafe { let sum = lasx_xvhaddw_h_b(sum, sum); let sum = lasx_xvhaddw_w_h(sum, sum); let sum = lasx_xvhaddw_d_w(sum, sum); let sum = lasx_xvhaddw_q_d(sum, sum); let tmp = lasx_xvpermi_q::<1>(sum, sum); let sum = lasx_xvadd_w(sum, tmp); lasx_xvpickve2gr_wu::<0>(sum) } } let lf = lasx_xvrepli_b(b'\n' as i32); let off = beg.align_offset(32); if off != 0 && off < end.offset_from_unsigned(beg) { (beg, line) = lines_fwd_fallback(beg, beg.add(off), line, line_stop); } if line < line_stop { while end.offset_from_unsigned(beg) >= 128 { let v1 = lasx_xvld::<0>(beg as *const _); let v2 = lasx_xvld::<32>(beg as *const _); let v3 = lasx_xvld::<64>(beg as *const _); let v4 = lasx_xvld::<96>(beg as *const _); let mut sum = lasx_xvrepli_b(0); sum = lasx_xvsub_b(sum, lasx_xvseq_b(v1, lf)); sum = lasx_xvsub_b(sum, lasx_xvseq_b(v2, lf)); sum = lasx_xvsub_b(sum, lasx_xvseq_b(v3, lf)); sum = lasx_xvsub_b(sum, lasx_xvseq_b(v4, lf)); let sum = horizontal_sum(sum); let line_next = line + sum as CoordType; if line_next >= line_stop { break; } beg = beg.add(128); line = line_next; } while end.offset_from_unsigned(beg) >= 32 { let v = lasx_xvld::<0>(beg as *const _); let c = lasx_xvseq_b(v, lf); let ones = lasx_xvand_v(c, lasx_xvrepli_b(1)); let sum = horizontal_sum(ones); let line_next = line + sum as CoordType; if line_next >= line_stop { break; } beg = beg.add(32); line = line_next; } } lines_fwd_fallback(beg, end, line, line_stop) } } #[cfg(target_arch = "loongarch64")] #[target_feature(enable = "lsx")] unsafe fn lines_fwd_lsx( mut beg: *const u8, end: *const u8, mut line: CoordType, line_stop: CoordType, ) -> (*const u8, CoordType) { unsafe { use std::arch::loongarch64::*; #[inline(always)] unsafe fn horizontal_sum(sum: m128i) -> u32 { unsafe { let sum = lsx_vhaddw_h_b(sum, sum); let sum = lsx_vhaddw_w_h(sum, sum); let sum = lsx_vhaddw_d_w(sum, sum); let sum = lsx_vhaddw_q_d(sum, sum); lsx_vpickve2gr_wu::<0>(sum) } } const LF: i32 = b'\n' as i32; let off = beg.align_offset(16); if off != 0 && off < end.offset_from_unsigned(beg) { (beg, line) = lines_fwd_fallback(beg, beg.add(off), line, line_stop); } if line < line_stop { while end.offset_from_unsigned(beg) >= 64 { let v1 = lsx_vld::<0>(beg as *const _); let v2 = lsx_vld::<16>(beg as *const _); let v3 = lsx_vld::<32>(beg as *const _); let v4 = lsx_vld::<48>(beg as *const _); let mut sum = lsx_vldi(0); sum = lsx_vsub_b(sum, lsx_vseqi_b::(v1)); sum = lsx_vsub_b(sum, lsx_vseqi_b::(v2)); sum = lsx_vsub_b(sum, lsx_vseqi_b::(v3)); sum = lsx_vsub_b(sum, lsx_vseqi_b::(v4)); let sum = horizontal_sum(sum); let line_next = line + sum as CoordType; if line_next >= line_stop { break; } beg = beg.add(64); line = line_next; } while end.offset_from_unsigned(beg) >= 16 { let v = lsx_vld::<0>(beg as *const _); let c = lsx_vseqi_b::(v); let ones = lsx_vandi_b::<1>(c); let sum = horizontal_sum(ones); let line_next = line + sum as CoordType; if line_next >= line_stop { break; } beg = beg.add(16); line = line_next; } } lines_fwd_fallback(beg, end, line, line_stop) } } #[cfg(target_arch = "aarch64")] unsafe fn lines_fwd_neon( mut beg: *const u8, end: *const u8, mut line: CoordType, line_stop: CoordType, ) -> (*const u8, CoordType) { unsafe { use std::arch::aarch64::*; let lf = vdupq_n_u8(b'\n'); let off = beg.align_offset(16); if off != 0 && off < end.offset_from_unsigned(beg) { (beg, line) = lines_fwd_fallback(beg, beg.add(off), line, line_stop); } if line < line_stop { while end.offset_from_unsigned(beg) >= 64 { let v1 = vld1q_u8(beg.add(0)); let v2 = vld1q_u8(beg.add(16)); let v3 = vld1q_u8(beg.add(32)); let v4 = vld1q_u8(beg.add(48)); // `vceqq_u8` leaves each comparison result byte as 0 or -1 (0xff). // This allows us to accumulate the comparisons by subtracting them. let mut sum = vdupq_n_u8(0); sum = vsubq_u8(sum, vceqq_u8(v1, lf)); sum = vsubq_u8(sum, vceqq_u8(v2, lf)); sum = vsubq_u8(sum, vceqq_u8(v3, lf)); sum = vsubq_u8(sum, vceqq_u8(v4, lf)); let sum = vaddvq_u8(sum); let line_next = line + sum as CoordType; if line_next >= line_stop { break; } beg = beg.add(64); line = line_next; } while end.offset_from_unsigned(beg) >= 16 { let v = vld1q_u8(beg); let c = vceqq_u8(v, lf); let c = vandq_u8(c, vdupq_n_u8(0x01)); let sum = vaddvq_u8(c); let line_next = line + sum as CoordType; if line_next >= line_stop { break; } beg = beg.add(16); line = line_next; } } lines_fwd_fallback(beg, end, line, line_stop) } } #[cfg(test)] mod test { use super::*; use crate::helpers::CoordType; use crate::simd::test::*; #[test] fn pseudo_fuzz() { let text = generate_random_text(1024); let lines = count_lines(&text); let mut offset_rng = make_rng(); let mut line_rng = make_rng(); let mut line_distance_rng = make_rng(); for _ in 0..1000 { let offset = offset_rng() % (text.len() + 1); let line = line_rng() % 100; let line_stop = (line + line_distance_rng() % (lines + 1)).saturating_sub(5); let line = line as CoordType; let line_stop = line_stop as CoordType; let expected = reference_lines_fwd(text.as_bytes(), offset, line, line_stop); let actual = lines_fwd(text.as_bytes(), offset, line, line_stop); assert_eq!(expected, actual); } } fn reference_lines_fwd( haystack: &[u8], mut offset: usize, mut line: CoordType, line_stop: CoordType, ) -> (usize, CoordType) { if line < line_stop { while offset < haystack.len() { let c = haystack[offset]; offset += 1; if c == b'\n' { line += 1; if line == line_stop { break; } } } } (offset, line) } } ================================================ FILE: crates/edit/src/simd/memchr2.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! `memchr`, but with two needles. use std::ptr; /// `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); it.offset_from_unsigned(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", target_arch = "loongarch64"))] return unsafe { MEMCHR2_DISPATCH(needle1, needle2, beg, end) }; #[cfg(target_arch = "aarch64")] return unsafe { memchr2_neon(needle1, needle2, beg, end) }; #[allow(unreachable_code)] return unsafe { memchr2_fallback(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", target_arch = "loongarch64"))] 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 = end.offset_from_unsigned(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 = "loongarch64")] unsafe fn memchr2_dispatch(needle1: u8, needle2: u8, beg: *const u8, end: *const u8) -> *const u8 { use std::arch::is_loongarch_feature_detected; let func = if is_loongarch_feature_detected!("lasx") { memchr2_lasx } else if is_loongarch_feature_detected!("lsx") { memchr2_lsx } else { memchr2_fallback }; unsafe { MEMCHR2_DISPATCH = func }; unsafe { func(needle1, needle2, beg, end) } } #[cfg(target_arch = "loongarch64")] #[target_feature(enable = "lasx")] unsafe fn memchr2_lasx(needle1: u8, needle2: u8, mut beg: *const u8, end: *const u8) -> *const u8 { unsafe { use std::arch::loongarch64::*; let n1 = lasx_xvreplgr2vr_b(needle1 as i32); let n2 = lasx_xvreplgr2vr_b(needle2 as i32); let off = beg.align_offset(32); if off != 0 && off < end.offset_from_unsigned(beg) { beg = memchr2_lsx(needle1, needle2, beg, beg.add(off)); } while end.offset_from_unsigned(beg) >= 32 { let v = lasx_xvld::<0>(beg as *const _); let a = lasx_xvseq_b(v, n1); let b = lasx_xvseq_b(v, n2); let c = lasx_xvor_v(a, b); let m = lasx_xvmskltz_b(c); let l = lasx_xvpickve2gr_wu::<0>(m); let h = lasx_xvpickve2gr_wu::<4>(m); let m = (h << 16) | l; if m != 0 { return beg.add(m.trailing_zeros() as usize); } beg = beg.add(32); } memchr2_fallback(needle1, needle2, beg, end) } } #[cfg(target_arch = "loongarch64")] #[target_feature(enable = "lsx")] unsafe fn memchr2_lsx(needle1: u8, needle2: u8, mut beg: *const u8, end: *const u8) -> *const u8 { unsafe { use std::arch::loongarch64::*; let n1 = lsx_vreplgr2vr_b(needle1 as i32); let n2 = lsx_vreplgr2vr_b(needle2 as i32); let off = beg.align_offset(16); if off != 0 && off < end.offset_from_unsigned(beg) { beg = memchr2_fallback(needle1, needle2, beg, beg.add(off)); } while end.offset_from_unsigned(beg) >= 16 { let v = lsx_vld::<0>(beg as *const _); let a = lsx_vseq_b(v, n1); let b = lsx_vseq_b(v, n2); let c = lsx_vor_v(a, b); let m = lsx_vmskltz_b(c); let m = lsx_vpickve2gr_wu::<0>(m); if m != 0 { return beg.add(m.trailing_zeros() as usize); } beg = beg.add(16); } memchr2_fallback(needle1, needle2, beg, end) } } #[cfg(target_arch = "aarch64")] unsafe fn memchr2_neon(needle1: u8, needle2: u8, mut beg: *const u8, end: *const u8) -> *const u8 { unsafe { use std::arch::aarch64::*; if end.offset_from_unsigned(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 end.offset_from_unsigned(beg) < 16 { break; } } } memchr2_fallback(needle1, needle2, beg, end) } } #[cfg(test)] mod tests { use std::slice; use stdext::sys::{virtual_commit, virtual_reserve}; use super::*; #[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 { const PAGE_SIZE: usize = 64 * 1024; // 64 KiB to cover many architectures. // 3 pages: uncommitted, committed, uncommitted let ptr = virtual_reserve(PAGE_SIZE * 3).unwrap(); virtual_commit(ptr.add(PAGE_SIZE), PAGE_SIZE).unwrap(); slice::from_raw_parts_mut(ptr.add(PAGE_SIZE).as_ptr(), PAGE_SIZE) }; 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); } } ================================================ FILE: crates/edit/src/simd/mod.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! Provides various high-throughput utilities. pub mod lines_bwd; pub mod lines_fwd; mod memchr2; pub use lines_bwd::*; pub use lines_fwd::*; pub use memchr2::*; #[cfg(test)] mod test { // Knuth's MMIX LCG pub fn make_rng() -> impl FnMut() -> usize { let mut state = 1442695040888963407u64; move || { state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); state as usize } } pub fn generate_random_text(len: usize) -> String { const ALPHABET: &[u8; 20] = b"0123456789abcdef\n\n\n\n"; let mut rng = make_rng(); let mut res = String::new(); for _ in 0..len { res.push(ALPHABET[rng() % ALPHABET.len()] as char); } res } pub fn count_lines(text: &str) -> usize { text.lines().count() } } ================================================ FILE: crates/edit/src/sys/mod.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! Platform abstractions. #[cfg(unix)] mod unix; #[cfg(windows)] mod windows; #[cfg(not(windows))] pub use std::fs::canonicalize; #[cfg(unix)] pub use unix::*; #[cfg(windows)] pub use windows::*; ================================================ FILE: crates/edit/src/sys/unix.rs ================================================ // 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::{c_char, c_int, c_void}; use std::fs::File; use std::mem::{self, ManuallyDrop, MaybeUninit}; use std::os::fd::{AsRawFd as _, FromRawFd as _}; use std::path::Path; use std::ptr::{NonNull, null_mut}; use std::{io, thread, time}; use stdext::arena::{Arena, scratch_arena}; use stdext::arena_format; use stdext::collections::{BString, BVec}; use crate::helpers::*; struct State { stdin: libc::c_int, stdin_flags: libc::c_int, stdout: libc::c_int, stdout_initial_termios: Option, 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() -> Deinit { Deinit } pub fn switch_modes() -> io::Result<()> { 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))?; // 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 *const () as libc::sighandler_t; check_int_return(libc::sigaction(libc::SIGWINCH, &sigwinch_action, null_mut()))?; // Get the original terminal modes so we can disable raw mode on exit. let mut termios = MaybeUninit::::uninit(); check_int_return(libc::tcgetattr(STATE.stdout, 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.stdout, libc::TCSANOW, &termios))?; Ok(()) } } pub struct Deinit; impl Drop for Deinit { fn drop(&mut self) { unsafe { #[allow(static_mut_refs)] if let Some(termios) = STATE.stdout_initial_termios.take() { // Restore the original terminal modes. libc::tcsetattr(STATE.stdout, libc::TCSANOW, &termios); } } } } pub fn inject_window_size_into_stdin() { unsafe { STATE.inject_resize = true; } } 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> { unsafe { if STATE.inject_resize { timeout = time::Duration::ZERO; } let read_poll = timeout != time::Duration::MAX; let mut buf = BVec::empty(); // 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(arena, 4 * KIBI); // We got some leftover broken UTF8 from a previous read? Prepend it. if STATE.utf8_len != 0 { buf.extend_from_slice(arena, &STATE.utf8_buf[..STATE.utf8_len]); STATE.utf8_len = 0; } 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 ret; #[cfg(target_os = "linux")] { let ts = libc::timespec { tv_sec: timeout.as_secs() as libc::time_t, tv_nsec: timeout.subsec_nanos() as libc::c_long, }; ret = libc::ppoll(&mut pollfd, 1, &ts, std::ptr::null()); } #[cfg(not(target_os = "linux"))] { ret = libc::poll(&mut pollfd, 1, timeout.as_millis() as libc::c_int); } 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 errno() { 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 = BString::from_utf8_lossy(arena, 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(arena, 0..0, &seq); } } 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 = errno(); 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 { 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 by handle or path. pub fn file_id(file: Option<&File>, path: &Path) -> io::Result { let file = match file { Some(f) => f, None => &File::open(path)?, }; unsafe { let mut stat = MaybeUninit::::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 }) } } unsafe fn load_library(name: *const c_char) -> io::Result> { unsafe { NonNull::new(libc::dlopen(name, libc::RTLD_LAZY)) .ok_or_else(|| from_raw_os_error(libc::ENOENT)) } } /// 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(handle: NonNull, name: *const c_char) -> io::Result { unsafe { let sym = libc::dlsym(handle.as_ptr(), name); if sym.is_null() { Err(from_raw_os_error(libc::ENOENT)) } else { Ok(mem::transmute_copy(&sym)) } } } pub struct LibIcu { pub libicuuc: NonNull, pub libicui18n: NonNull, } pub fn load_icu() -> io::Result { const fn const_str_eq(a: &str, b: &str) -> bool { let a = a.as_bytes(); let b = b.as_bytes(); let mut i = 0; loop { if i >= a.len() || i >= b.len() { return a.len() == b.len(); } if a[i] != b[i] { return false; } i += 1; } } const LIBICUUC: &str = concat!(env!("EDIT_CFG_ICUUC_SONAME"), "\0"); const LIBICUI18N: &str = concat!(env!("EDIT_CFG_ICUI18N_SONAME"), "\0"); if const { const_str_eq(LIBICUUC, LIBICUI18N) } { let icu = unsafe { load_library(LIBICUUC.as_ptr() as *const _)? }; Ok(LibIcu { libicuuc: icu, libicui18n: icu }) } else { let libicuuc = unsafe { load_library(LIBICUUC.as_ptr() as *const _)? }; let libicui18n = unsafe { load_library(LIBICUI18N.as_ptr() as *const _)? }; Ok(LibIcu { libicuuc, libicui18n }) } } /// ICU, by default, adds the major version as a suffix to each exported symbol. /// They also recommend to disable this for system-level installations (`runConfigureICU Linux --disable-renaming`), /// but I found that many (most?) Linux distributions don't do this for some reason. /// This function returns the suffix, if any. #[cfg(edit_icu_renaming_auto_detect)] pub fn icu_detect_renaming_suffix(arena: &Arena, handle: NonNull) -> BString<'_> { unsafe { type T = *const c_void; let mut res = BString::empty(); // Check if the ICU library is using unversioned symbols. // Return an empty suffix in that case. if get_proc_address::(handle, c"u_errorName".as_ptr()).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::(handle, c"_ZN8UCaseMapD1Ev".as_ptr()) { 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 std::ffi::CStr::from_ptr(info.dli_fname).to_str() { Ok(name) => name, Err(_) => return res, }; let path = match std::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(arena, '_'); res.push_str(arena, version); res } } #[cfg(edit_icu_renaming_auto_detect)] #[allow(clippy::not_unsafe_ptr_arg_deref)] pub fn icu_add_renaming_suffix<'a, 'b, 'r>( arena: &'a Arena, name: *const c_char, suffix: &str, ) -> *const c_char where 'a: 'r, 'b: 'r, { if suffix.is_empty() { name } else { // SAFETY: In this particular case we know that the string // is valid UTF-8, because it comes from icu.rs. let name = unsafe { std::ffi::CStr::from_ptr(name) }; let name = unsafe { name.to_str().unwrap_unchecked() }; let mut res = BString::empty(); res.reserve(arena, name.len() + suffix.len() + 1); res.push_str(arena, name); res.push_str(arena, suffix); res.push(arena, '\0'); res.as_ptr() as *const c_char } } pub fn preferred_languages(arena: &Arena) -> BVec<'_, BString<'_>> { let mut locales = BVec::empty(); for key in ["LANGUAGE", "LC_ALL", "LANG"] { if let Ok(val) = std::env::var(key) && !val.is_empty() { locales.extend_sloppy( arena, val.split(':').filter(|s| !s.is_empty()).map(|s| { // Replace all underscores with dashes, // because the localization code expects pt-br, not pt_BR. let mut res = BVec::empty(); res.extend( arena, s.as_bytes().iter().map(|&b| if b == b'_' { b'-' } else { b }), ); unsafe { BString::from_utf8_unchecked(res) } }), ); break; } } locales } #[inline] #[cold] fn errno() -> c_int { // libc unfortunately doesn't export an alias for `errno` (WHY?). // As such we (ab)use the stdlib and use its internal errno implementation. // // Under `-O -Copt-level=s` the 1.87 compiler fails to fully inline and // remove the raw_os_error() call. This leaves us with the drop() call. // ManuallyDrop fixes that and results in a direct `std::sys::os::errno` call. ManuallyDrop::new(io::Error::last_os_error()).raw_os_error().unwrap_or(0) } #[inline] #[cold] fn last_os_error() -> io::Error { io::Error::last_os_error() } #[inline] #[cold] fn from_raw_os_error(code: c_int) -> io::Error { io::Error::from_raw_os_error(code) } fn check_int_return(ret: libc::c_int) -> io::Result { if ret < 0 { Err(last_os_error()) } else { Ok(ret) } } ================================================ FILE: crates/edit/src/sys/windows.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use std::ffi::{OsString, c_char, c_void}; 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::{io, mem, time}; use stdext::arena::{Arena, scratch_arena}; use stdext::arena_write_fmt; use stdext::collections::{BString, BVec}; use windows_sys::Win32::Storage::FileSystem; use windows_sys::Win32::System::{Console, IO, LibraryLoader, Threading}; use windows_sys::Win32::{Foundation, Globalization}; use windows_sys::core::*; use crate::helpers::*; macro_rules! w_env { ($s:literal) => {{ const INPUT: &[u8] = env!($s).as_bytes(); const OUTPUT_LEN: usize = windows_sys::core::utf16_len(INPUT) + 1; const OUTPUT: &[u16; OUTPUT_LEN] = { let mut buffer = [0; OUTPUT_LEN]; let mut input_pos = 0; let mut output_pos = 0; while let Some((mut code_point, new_pos)) = windows_sys::core::decode_utf8_char(INPUT, input_pos) { input_pos = new_pos; if code_point <= 0xffff { buffer[output_pos] = code_point as u16; output_pos += 1; } else { code_point -= 0x10000; buffer[output_pos] = 0xd800 + (code_point >> 10) as u16; output_pos += 1; buffer[output_pos] = 0xdc00 + (code_point & 0x3ff) as u16; output_pos += 1; } } &{ buffer } }; OUTPUT.as_ptr() }}; } type ReadConsoleInputExW = unsafe extern "system" fn( h_console_input: Foundation::HANDLE, lp_buffer: *mut Console::INPUT_RECORD, n_length: u32, lp_number_of_events_read: *mut u32, w_flags: u16, ) -> BOOL; unsafe extern "system" fn read_console_input_ex_placeholder( _: Foundation::HANDLE, _: *mut Console::INPUT_RECORD, _: u32, _: *mut u32, _: u16, ) -> 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) -> BOOL { unsafe { STATE.wants_exit = true; IO::CancelIoEx(STATE.stdin, null()); } 1 } /// Initializes the platform-specific state. pub fn init() -> Deinit { unsafe { // Get the stdin and stdout handles first, so that if this function fails, // we at least got something to use for `write_stdout`. STATE.stdin = Console::GetStdHandle(Console::STD_INPUT_HANDLE); STATE.stdout = Console::GetStdHandle(Console::STD_OUTPUT_HANDLE); Deinit } } /// Switches the terminal into raw mode, etc. pub fn switch_modes() -> io::Result<()> { unsafe { // `kernel32.dll` doesn't exist on OneCore variants of Windows. // NOTE: `kernelbase.dll` is NOT a stable API to rely on. In our case it's the best option though. // // This is written as two nested `match` statements so that we can return the error from the first // `load_read_func` call if it fails. The kernel32.dll lookup may contain some valid information, // while the kernelbase.dll lookup may not, since it's not a stable API. unsafe fn load_read_func(module: *const u16) -> io::Result { unsafe { get_module(module) .and_then(|m| get_proc_address(m, c"ReadConsoleInputExW".as_ptr())) } } STATE.read_console_input_ex = match load_read_func(w!("kernel32.dll")) { Ok(func) => func, Err(err) => match load_read_func(w!("kernelbase.dll")) { Ok(func) => func, Err(_) => return Err(err), }, }; // Reopen stdin if it's redirected (= piped input). if ptr::eq(STATE.stdin, Foundation::INVALID_HANDLE_VALUE) || !matches!(FileSystem::GetFileType(STATE.stdin), FileSystem::FILE_TYPE_CHAR) { 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(last_os_error()); } 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))?; match check_bool_return(Console::SetConsoleMode( STATE.stdin, Console::ENABLE_WINDOW_INPUT | Console::ENABLE_EXTENDED_FLAGS | Console::ENABLE_VIRTUAL_TERMINAL_INPUT, )) { Err(e) if e.kind() == io::ErrorKind::InvalidInput => { Err(io::Error::other("This application does not support the legacy console.")) } other => other, }?; check_bool_return(Console::SetConsoleMode( STATE.stdout, Console::ENABLE_PROCESSED_OUTPUT | Console::ENABLE_WRAP_AT_EOL_OUTPUT | Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING | Console::DISABLE_NEWLINE_AUTO_RETURN, ))?; check_bool_return(Console::SetConsoleCtrlHandler(Some(console_ctrl_handler), 1))?; STATE.stdin_cp_old = Console::GetConsoleCP(); STATE.stdout_cp_old = Console::GetConsoleOutputCP(); check_bool_return(Console::SetConsoleCP(Globalization::CP_UTF8))?; check_bool_return(Console::SetConsoleOutputCP(Globalization::CP_UTF8))?; Ok(()) } } 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; } } } } /// 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 { unsafe { let mut info: Console::CONSOLE_SCREEN_BUFFER_INFOEX = mem::zeroed(); info.cbSize = mem::size_of::() 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> { 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 = BString::empty(); text.reserve(arena, 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. arena_write_fmt!(arena, 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); } } } 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 { 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 } } } pub fn drives() -> impl Iterator { unsafe { let mut mask = FileSystem::GetLogicalDrives(); std::iter::from_fn(move || { let bit = mask.trailing_zeros(); if bit >= 26 { None } else { mask &= !(1 << bit); Some((b'A' + bit as u8) as char) } }) } } /// A unique identifier for a file. pub enum FileId { Id(FileSystem::FILE_ID_INFO), Path(PathBuf), } impl PartialEq for FileId { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::Id(left), Self::Id(right)) => { // Lowers to an efficient word-wise comparison. const SIZE: usize = std::mem::size_of::(); let a: &[u8; SIZE] = unsafe { mem::transmute(left) }; let b: &[u8; SIZE] = unsafe { mem::transmute(right) }; a == b } (Self::Path(left), Self::Path(right)) => left == right, _ => false, } } } impl Eq for FileId {} /// Returns a unique identifier for the given file by handle or path. pub fn file_id(file: Option<&File>, path: &Path) -> io::Result { let file = match file { Some(f) => f, None => &File::open(path)?, }; file_id_from_handle(file).or_else(|_| Ok(FileId::Path(std::fs::canonicalize(path)?))) } fn file_id_from_handle(file: &File) -> io::Result { unsafe { let mut info = MaybeUninit::::uninit(); check_bool_return(FileSystem::GetFileInformationByHandleEx( file.as_raw_handle(), FileSystem::FileIdInfo, info.as_mut_ptr() as *mut _, mem::size_of::() as u32, ))?; Ok(FileId::Id(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 { 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) } unsafe fn get_module(name: *const u16) -> io::Result> { unsafe { check_ptr_return(LibraryLoader::GetModuleHandleW(name)) } } unsafe fn load_library(name: *const u16) -> io::Result> { 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(handle: NonNull, name: *const c_char) -> io::Result { unsafe { let ptr = LibraryLoader::GetProcAddress(handle.as_ptr(), name as *const u8); if let Some(ptr) = ptr { Ok(mem::transmute_copy(&ptr)) } else { Err(last_os_error()) } } } pub struct LibIcu { pub libicuuc: NonNull, pub libicui18n: NonNull, } pub fn load_icu() -> io::Result { const fn const_ptr_u16_eq(a: *const u16, b: *const u16) -> bool { unsafe { let mut a = a; let mut b = b; loop { if *a != *b { return false; } if *a == 0 { return true; } a = a.add(1); b = b.add(1); } } } const LIBICUUC: *const u16 = w_env!("EDIT_CFG_ICUUC_SONAME"); const LIBICUI18N: *const u16 = w_env!("EDIT_CFG_ICUI18N_SONAME"); if const { const_ptr_u16_eq(LIBICUUC, LIBICUI18N) } { let icu = unsafe { load_library(LIBICUUC)? }; Ok(LibIcu { libicuuc: icu, libicui18n: icu }) } else { let libicuuc = unsafe { load_library(LIBICUUC)? }; let libicui18n = unsafe { load_library(LIBICUI18N)? }; Ok(LibIcu { libicuuc, libicui18n }) } } /// Returns a list of preferred languages for the current user. pub fn preferred_languages<'a>(arena: &'a Arena) -> BVec<'a, &'a str> { // 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)); // 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 langs = BString::from_utf16_lossy(arena, langs).leak(); // Split the null-delimited string into individual chunks // and copy them into the given arena. let mut res = BVec::empty(); res.extend_sloppy(arena, langs.split_terminator('\0').filter(|s| !s.is_empty())); res } #[inline] #[cold] fn last_os_error() -> io::Error { io::Error::last_os_error() } fn check_bool_return(ret: BOOL) -> io::Result<()> { if ret == 0 { Err(last_os_error()) } else { Ok(()) } } fn check_ptr_return(ret: *mut T) -> io::Result> { NonNull::new(ret).ok_or_else(last_os_error) } ================================================ FILE: crates/edit/src/tui.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! An immediate mode UI framework for terminals. //! //! # Why immediate mode? //! //! This uses an "immediate mode" design, similar to [ImGui](https://github.com/ocornut/imgui). //! The reason for this is that I expect the UI needs for any terminal application to be //! fairly minimal, and for that purpose an immediate mode design is much simpler to use. //! //! So what's "immediate mode"? The primary alternative is called "retained mode". //! The difference is that when you create a button in this framework in one frame, //! and you stop telling this framework in the next frame, the button will vanish. //! When you use a regular retained mode UI framework, you create the button once, //! set up callbacks for when it is clicked, and then stop worrying about it. //! //! The downside of immediate mode is that your UI code _may_ become cluttered. //! The upside however is that you cannot leak UI elements, you don't need to //! worry about lifetimes nor callbacks, and that simple UIs are simple to write. //! //! More importantly though, the primary reason for this is that the //! lack of callbacks means we can use this design across a plain C ABI, //! which we'll need once plugins come into play. GTK's `g_signal_connect` //! shows that the alternative can be rather cumbersome. //! //! # Design overview //! //! While this file is fairly lengthy, the overall algorithm is simple. //! On the first frame ever: //! * Prepare an empty `arena_next`. //! * Parse the incoming [`input::Input`] which should be a resize event. //! * Create a new [`Context`] instance and give it the caller. //! * Now the caller will draw their UI with the [`Context`] by calling the //! various [`Context`] UI methods, such as [`Context::block_begin()`] and //! [`Context::block_end()`]. These two are the basis which all other UI //! elements are built upon by the way. Each UI element that is created gets //! allocated onto `arena_next` and inserted into the UI tree. //! That tree works exactly like the DOM tree in HTML: Each node in the tree //! has a parent, children, and siblings. The tree layout at the end is then //! a direct mirror of the code "layout" that created it. //! * Once the caller is done and drops the [`Context`], it'll secretly call //! `report_context_completion`. This causes a number of things: //! * The DOM tree that was built is stored in `prev_tree`. //! * A hashmap of all nodes is built and stored in `prev_node_map`. //! * `arena_next` is swapped with `arena_prev`. //! * Each UI node is measured and laid out. //! * Now the caller is expected to repeat this process with a [`None`] //! input event until [`Tui::needs_settling()`] returns false. //! This is necessary, because when [`Context::button()`] returns `true` //! in one frame, it may change the state in the caller's code //! and require another frame to be drawn. //! * Finally a call to [`Tui::render()`] will render the UI tree into the //! framebuffer and return VT output. //! //! On every subsequent frame the process is similar, but one crucial element //! of any immediate mode UI framework is added: //! Now when the caller draws their UI, the various [`Context`] UI elements //! have access to `prev_node_map` and the previously built UI tree. //! This allows the UI framework to reuse the previously computed layout for //! hit tests, caching scroll offsets, and so on. //! //! In the end it looks very similar: //! * Prepare an empty `arena_next`. //! * Parse the incoming [`input::Input`]... //! * **BUT** now we can hit-test mouse clicks onto the previously built //! UI tree. This way we can delegate focus on left mouse clicks. //! * Create a new [`Context`] instance and give it the caller. //! * The caller draws their UI with the [`Context`]... //! * **BUT** we can preserve the UI state across frames. //! * Continue rendering until [`Tui::needs_settling()`] returns false. //! * And the final call to [`Tui::render()`]. //! //! # Classnames and node IDs //! //! So how do we find which node from the previous tree correlates to the //! current node? Each node needs to be constructed with a "classname". //! The classname is hashed with the parent node ID as the seed. This derived //! hash is then used as the new child node ID. Under the assumption that the //! collision likelihood of the hash function is low, this serves as true IDs. //! //! This has the nice added property that finding a node with the same ID //! guarantees that all of the parent nodes must have equivalent IDs as well. //! This turns "is the focus anywhere inside this subtree" into an O(1) check. //! //! The reason "classnames" are used is because I was hoping to add theming //! in the future with a syntax similar to CSS (simplified, however). //! //! # Example //! //! ``` //! use edit::helpers::Size; //! use edit::input::Input; //! use edit::tui::*; //! use stdext::{arena, arena_format}; //! //! struct State { //! counter: i32, //! } //! //! fn main() { //! arena::init(128 * 1024 * 1024).unwrap(); //! //! // Create a `Tui` instance which holds state across frames. //! let mut tui = Tui::new().unwrap(); //! let mut state = State { counter: 0 }; //! let input = Input::Resize(Size { width: 80, height: 24 }); //! //! // Pass the input to the TUI. //! { //! let mut ctx = tui.create_context(Some(input)); //! draw(&mut ctx, &mut state); //! } //! //! // Continue until the layout has settled. //! while tui.needs_settling() { //! let mut ctx = tui.create_context(None); //! draw(&mut ctx, &mut state); //! } //! //! // Render the output. //! let scratch = arena::scratch_arena(None); //! let output = tui.render(&*scratch); //! println!("{}", output); //! } //! //! fn draw(ctx: &mut Context, state: &mut State) { //! ctx.table_begin("classname"); //! { //! ctx.table_next_row(); //! //! // Thanks to the lack of callbacks, we can use a primitive //! // if condition here, as well as in any potential C code. //! if ctx.button("button", "Click me!", ButtonStyle::default()) { //! state.counter += 1; //! } //! //! // Similarly, formatting and showing labels is straightforward. //! // It's impossible to forget updating the label this way. //! ctx.label("label", &arena_format!(ctx.arena(), "Counter: {}", state.counter)); //! } //! ctx.table_end(); //! } //! ``` #[cfg(debug_assertions)] use std::collections::HashSet; use std::{io, iter, mem, ptr, time}; use stdext::arena::{Arena, scratch_arena}; use stdext::collections::{BString, BVec}; use stdext::{arena_format, arena_write_fmt, opt_ptr_eq, str_from_raw_parts}; use crate::buffer::{CursorMovement, MoveLineDirection, RcTextBuffer, TextBuffer, TextBufferCell}; use crate::cell::*; use crate::clipboard::Clipboard; use crate::document::WriteableDocument; use crate::framebuffer::{Attributes, Framebuffer, INDEXED_COLORS_COUNT, IndexedColor}; use crate::hash::*; use crate::helpers::*; use crate::input::{InputKeyMod, kbmod, vk}; use crate::oklab::StraightRgba; use crate::{input, simd, unicode}; const ROOT_ID: u64 = 0x14057B7EF767814F; // Knuth's MMIX constant const SHIFT_TAB: InputKey = vk::TAB.with_modifiers(kbmod::SHIFT); const KBMOD_FOR_WORD_NAV: InputKeyMod = if cfg!(any(target_os = "macos", target_os = "ios")) { kbmod::ALT } else { kbmod::CTRL }; type Input<'input> = input::Input<'input>; type InputKey = input::InputKey; type InputMouseState = input::InputMouseState; /// Since [`TextBuffer`] creation and management is expensive, /// we cache instances of them for reuse between frames. /// This is used for [`Context::editline()`]. struct CachedTextBuffer { node_id: u64, editor: RcTextBuffer, seen: bool, } /// Since [`Context::editline()`] and [`Context::textarea()`] /// do almost the same thing, this abstracts over the two. enum TextBufferPayload<'a> { Editline(&'a mut dyn WriteableDocument), Textarea(RcTextBuffer), } /// In order for the TUI to show the correct Ctrl/Alt/Shift /// translations, this struct lets you set them. pub struct ModifierTranslations { pub ctrl: &'static str, pub alt: &'static str, pub shift: &'static str, } /// Controls to which node the floater is anchored. #[derive(Default, Clone, Copy, PartialEq, Eq)] pub enum Anchor { /// The floater is attached relative to the node created last. #[default] Last, /// The floater is attached relative to the current node (= parent of new nodes). Parent, /// The floater is attached relative to the root node (= usually the viewport). Root, } /// Controls the position of the floater. See [`Context::attr_float`]. #[derive(Default)] pub struct FloatSpec { /// Controls to which node the floater is anchored. pub anchor: Anchor, // Specifies the origin of the container relative to the container size. [0, 1] pub gravity_x: f32, pub gravity_y: f32, // Specifies an offset from the origin in cells. pub offset_x: f32, pub offset_y: f32, } /// Informs you about the change that was made to the list selection. #[derive(Clone, Copy, PartialEq, Eq)] pub enum ListSelection { /// The selection wasn't changed. Unchanged, /// The selection was changed to the current list item. Selected, /// The selection was changed to the current list item /// *and* the item was also activated (Enter or Double-click). Activated, } /// Controls the position of a node relative to its parent. #[derive(Default)] pub enum Position { /// The child is stretched to fill the parent. #[default] Stretch, /// The child is positioned at the left edge of the parent. Left, /// The child is positioned at the center of the parent. Center, /// The child is positioned at the right edge of the parent. Right, } /// Controls the text overflow behavior of a label /// when the text doesn't fit the container. #[derive(Default, Clone, Copy, PartialEq, Eq)] pub enum Overflow { /// Text is simply cut off when it doesn't fit. #[default] Clip, /// An ellipsis is shown at the end of the text. TruncateHead, /// An ellipsis is shown in the middle of the text. TruncateMiddle, /// An ellipsis is shown at the beginning of the text. TruncateTail, } /// Controls the style with which a button label renders #[derive(Clone, Copy)] pub struct ButtonStyle { accelerator: Option, checked: Option, bracketed: bool, } impl ButtonStyle { /// Draw an accelerator label: `[_E_xample button]` or `[Example button(X)]` /// /// Must provide an upper-case ASCII character. pub fn accelerator(self, char: char) -> Self { Self { accelerator: Some(char), ..self } } /// Draw a checkbox prefix: `[🗹 Example Button]` pub fn checked(self, checked: bool) -> Self { Self { checked: Some(checked), ..self } } /// Draw with or without brackets: `[Example Button]` or `Example Button` pub fn bracketed(self, bracketed: bool) -> Self { Self { bracketed, ..self } } } impl Default for ButtonStyle { fn default() -> Self { Self { accelerator: None, checked: None, bracketed: true, // Default style for most buttons. Brackets may be disabled e.g. for buttons in menus } } } /// There's two types of lifetimes the TUI code needs to manage: /// * Across frames /// * Per frame /// /// [`Tui`] manages the first one. It's also the entrypoint for /// everything else you may want to do. pub struct Tui { /// Arena used for the previous frame. arena_prev: Arena, /// Arena used for the current frame. arena_next: Arena, /// The UI tree built in the previous frame. /// This refers to memory in `arena_prev`. prev_tree: Tree<'static>, /// A hashmap of all nodes built in the previous frame. /// This refers to memory in `arena_prev`. prev_node_map: NodeMap<'static>, /// The framebuffer used for rendering. framebuffer: Framebuffer, modifier_translations: ModifierTranslations, floater_default_bg: StraightRgba, floater_default_fg: StraightRgba, modal_default_bg: StraightRgba, modal_default_fg: StraightRgba, /// Last known terminal size. /// /// This lives here instead of [`Context`], because we need to /// track the state across frames and input events. /// This also applies to the remaining members in this block below. size: Size, /// Last known mouse position. mouse_position: Point, /// Between mouse down and up, the position where the mouse was pressed. /// Otherwise, this contains Point::MIN. mouse_down_position: Point, /// Node ID of the node that was clicked on. /// Used for tracking drag targets. left_mouse_down_target: u64, /// Timestamp of the last mouse up event. /// Used for tracking double/triple clicks. mouse_up_timestamp: std::time::Instant, /// The current mouse state. mouse_state: InputMouseState, /// Whether the mouse is currently being dragged. mouse_is_drag: bool, /// The number of clicks that have happened in a row. /// Gets reset when the mouse was released for a while. mouse_click_counter: CoordType, /// The path to the node that was clicked on. mouse_down_node_path: Vec, /// The position of the first click in a double/triple click series. first_click_position: Point, /// The node ID of the node that was first clicked on /// in a double/triple click series. first_click_target: u64, /// Path to the currently focused node. focused_node_path: Vec, /// Contains the last element in [`Tui::focused_node_path`]. /// This way we can track if the focus changed, because then we /// need to scroll the node into view if it's within a scrollarea. focused_node_for_scrolling: u64, /// A list of cached text buffers used for [`Context::editline()`]. cached_text_buffers: Vec, /// The clipboard contents. clipboard: Clipboard, settling_have: i32, settling_want: i32, read_timeout: time::Duration, } impl Tui { /// Creates a new [`Tui`] instance for storing state across frames. pub fn new() -> io::Result { let arena_prev = Arena::new(128 * MEBI)?; let arena_next = Arena::new(128 * MEBI)?; // SAFETY: Since `prev_tree` refers to `arena_prev`/`arena_next`, from its POV the lifetime // is `'static`, requiring us to use `transmute` to circumvent the borrow checker. let prev_tree = Tree::new(unsafe { mem::transmute::<&Arena, &Arena>(&arena_next) }); let mut tui = Self { arena_prev, arena_next, prev_tree, prev_node_map: Default::default(), framebuffer: Framebuffer::new(), modifier_translations: ModifierTranslations { ctrl: "Ctrl", alt: "Alt", shift: "Shift", }, floater_default_bg: StraightRgba::zero(), floater_default_fg: StraightRgba::zero(), modal_default_bg: StraightRgba::zero(), modal_default_fg: StraightRgba::zero(), size: Size { width: 0, height: 0 }, mouse_position: Point::MIN, mouse_down_position: Point::MIN, left_mouse_down_target: 0, mouse_up_timestamp: std::time::Instant::now(), mouse_state: InputMouseState::None, mouse_is_drag: false, mouse_click_counter: 0, mouse_down_node_path: Vec::with_capacity(16), first_click_position: Point::MIN, first_click_target: 0, focused_node_path: Vec::with_capacity(16), focused_node_for_scrolling: ROOT_ID, cached_text_buffers: Vec::with_capacity(16), clipboard: Default::default(), settling_have: 0, settling_want: 0, read_timeout: time::Duration::MAX, }; Self::clean_node_path(&mut tui.mouse_down_node_path); Self::clean_node_path(&mut tui.focused_node_path); Ok(tui) } /// Sets up the framebuffer's color palette. pub fn setup_indexed_colors(&mut self, colors: [StraightRgba; INDEXED_COLORS_COUNT]) { self.framebuffer.set_indexed_colors(colors); } /// Set up translations for Ctrl/Alt/Shift modifiers. pub fn setup_modifier_translations(&mut self, translations: ModifierTranslations) { self.modifier_translations = translations; } /// Set the default background color for floaters (dropdowns, etc.). pub fn set_floater_default_bg(&mut self, color: StraightRgba) { self.floater_default_bg = color; } /// Set the default foreground color for floaters (dropdowns, etc.). pub fn set_floater_default_fg(&mut self, color: StraightRgba) { self.floater_default_fg = color; } /// Set the default background color for modals. pub fn set_modal_default_bg(&mut self, color: StraightRgba) { self.modal_default_bg = color; } /// Set the default foreground color for modals. pub fn set_modal_default_fg(&mut self, color: StraightRgba) { self.modal_default_fg = color; } /// If the TUI is currently running animations, etc., /// this will return a timeout smaller than [`time::Duration::MAX`]. pub fn read_timeout(&mut self) -> time::Duration { mem::replace(&mut self.read_timeout, time::Duration::MAX) } /// Returns the viewport size. pub fn size(&self) -> Size { // We don't use the size stored in the framebuffer, because until // `render()` is called, the framebuffer will use a stale size. self.size } /// Returns an indexed color from the framebuffer. #[inline] pub fn indexed(&self, index: IndexedColor) -> StraightRgba { self.framebuffer.indexed(index) } /// Returns an indexed color from the framebuffer with the given alpha. /// See [`Framebuffer::indexed_alpha()`]. #[inline] pub fn indexed_alpha( &self, index: IndexedColor, numerator: u32, denominator: u32, ) -> StraightRgba { self.framebuffer.indexed_alpha(index, numerator, denominator) } /// Returns a color in contrast with the given color. /// See [`Framebuffer::contrasted()`]. pub fn contrasted(&self, color: StraightRgba) -> StraightRgba { self.framebuffer.contrasted(color) } /// Returns the clipboard. pub fn clipboard_ref(&self) -> &Clipboard { &self.clipboard } /// Returns the clipboard (mutable). pub fn clipboard_mut(&mut self) -> &mut Clipboard { &mut self.clipboard } /// Starts a new frame and returns a [`Context`] for it. pub fn create_context<'a, 'input>( &'a mut self, input: Option>, ) -> Context<'a, 'input> { // SAFETY: Since we have a unique `&mut self`, nothing is holding onto `arena_prev`, // which will become `arena_next` and get reset. It's safe to reset and reuse its memory. mem::swap(&mut self.arena_prev, &mut self.arena_next); unsafe { self.arena_next.reset(0) }; // In the input handler below we transformed a mouse up into a release event. // Now, a frame later, we must reset it back to none, to stop it from triggering things. // Same for Scroll events. if self.mouse_state > InputMouseState::Right { self.mouse_down_position = Point::MIN; self.mouse_down_node_path.clear(); self.left_mouse_down_target = 0; self.mouse_state = InputMouseState::None; self.mouse_is_drag = false; } let now = std::time::Instant::now(); let mut input_text = None; let mut input_keyboard = None; let mut input_mouse_modifiers = kbmod::NONE; let mut input_mouse_click = 0; let mut input_scroll_delta = Point { x: 0, y: 0 }; // `input_consumed` should be `true` if we're in the settling phase which is indicated by // `self.needs_settling() == true`. However, there's a possibility for it being true from // a previous frame, and we do have fresh new input. In that case want `input_consumed` // to be false of course which is ensured by checking for `input.is_none()`. let input_consumed = self.needs_settling() && input.is_none(); if self.scroll_to_focused() { self.needs_more_settling(); } match input { None => {} Some(Input::Resize(resize)) => { assert!(resize.width > 0 && resize.height > 0); assert!(resize.width < 32768 && resize.height < 32768); self.size = resize; } Some(Input::Text(text)) => { input_text = Some(text); // TODO: the .len()==1 check causes us to ignore keyboard inputs that are faster than we process them. // For instance, imagine the user presses "A" twice and we happen to read it in a single chunk. // This causes us to ignore the keyboard input here. We need a way to inform the caller over // how much of the input text we actually processed in a single frame. Or perhaps we could use // the needs_settling logic? if text.len() == 1 { let ch = text.as_bytes()[0]; input_keyboard = InputKey::from_ascii(ch as char) } } Some(Input::Paste(paste)) => { let clipboard = self.clipboard_mut(); clipboard.write(paste); clipboard.mark_as_synchronized(); input_keyboard = Some(kbmod::CTRL | vk::V); } Some(Input::Keyboard(keyboard)) => { input_keyboard = Some(keyboard); } Some(Input::Mouse(mouse)) => { let mut next_state = mouse.state; let next_position = mouse.position; let next_scroll = mouse.scroll; let mouse_down = self.mouse_state == InputMouseState::None && next_state != InputMouseState::None; let mouse_up = self.mouse_state != InputMouseState::None && next_state == InputMouseState::None; let is_scroll = next_scroll != Point::default(); let is_drag = self.mouse_state == InputMouseState::Left && next_state == InputMouseState::Left && next_position != self.mouse_position; let mut hovered_node = None; // Needed for `mouse_down` let mut focused_node = None; // Needed for `mouse_down` and `is_click` if mouse_down || mouse_up { // Roots (aka windows) are ordered in Z order, so we iterate // them in reverse order, from topmost to bottommost. for root in self.prev_tree.iterate_roots_rev() { // Find the node that contains the cursor. Tree::visit_all(root, root, true, |node| { let n = node.borrow(); if !n.outer_clipped.contains(next_position) { // Skip the entire sub-tree, because it doesn't contain the cursor. return VisitControl::SkipChildren; } hovered_node = Some(node); if n.attributes.focusable { focused_node = Some(node); } VisitControl::Continue }); // This root/window contains the cursor. // We don't care about any lower roots. if hovered_node.is_some() { break; } // This root is modal and swallows all clicks, // no matter whether the click was inside it or not. if matches!(root.borrow().content, NodeContent::Modal(_)) { break; } } } if is_scroll { next_state = self.mouse_state; } else if is_drag { self.mouse_is_drag = true; } else if mouse_down { // Transition from no mouse input to some mouse input --> Record the mouse down position. Self::build_node_path(hovered_node, &mut self.mouse_down_node_path); // On left-mouse-down we change focus. let mut target = 0; if next_state == InputMouseState::Left { target = focused_node.map_or(0, |n| n.borrow().id); Self::build_node_path(focused_node, &mut self.focused_node_path); self.needs_more_settling(); // See `needs_more_settling()`. } // Double-/Triple-/Etc.-clicks are triggered on mouse-down, // unlike the first initial click, which is triggered on mouse-up. if self.mouse_click_counter != 0 { if self.first_click_target != target || self.first_click_position != next_position || (now - self.mouse_up_timestamp) > std::time::Duration::from_millis(500) { // If the cursor moved / the focus changed in between, or if the user did a slow click, // we reset the click counter. On mouse-up it'll transition to a regular click. self.mouse_click_counter = 0; self.first_click_position = Point::MIN; self.first_click_target = 0; } else { self.mouse_click_counter += 1; input_mouse_click = self.mouse_click_counter; }; } // Gets reset at the start of this function. self.left_mouse_down_target = target; self.mouse_down_position = next_position; } else if mouse_up { // Transition from some mouse input to no mouse input --> The mouse button was released. next_state = InputMouseState::Release; let target = focused_node.map_or(0, |n| n.borrow().id); if self.left_mouse_down_target == 0 || self.left_mouse_down_target != target { // If `left_mouse_down_target == 0`, then it wasn't a left-click, in which case // the target gets reset. Same, if the focus changed in between any clicks. self.mouse_click_counter = 0; self.first_click_position = Point::MIN; self.first_click_target = 0; } else if self.mouse_click_counter == 0 { // No focus change, and no previous clicks? This is an initial, regular click. self.mouse_click_counter = 1; self.first_click_position = self.mouse_down_position; self.first_click_target = target; input_mouse_click = 1; } self.mouse_up_timestamp = now; } input_mouse_modifiers = mouse.modifiers; input_scroll_delta = next_scroll; self.mouse_position = next_position; self.mouse_state = next_state; } } if !input_consumed { // Every time there's input, we naturally need to re-render at least once. self.settling_have = 0; self.settling_want = 1; } // TODO: There should be a way to do this without unsafe. // Allocating from the arena borrows the arena, and so allocating the tree here borrows self. // This conflicts with us passing a mutable reference to `self` into the struct below. let tree = Tree::new(unsafe { mem::transmute::<&Arena, &Arena>(&self.arena_next) }); Context { tui: self, input_text, input_keyboard, input_mouse_modifiers, input_mouse_click, input_scroll_delta, input_consumed, tree, last_modal: None, focused_node: None, next_block_id_mixin: 0, needs_settling: false, #[cfg(debug_assertions)] seen_ids: HashSet::new(), } } fn report_context_completion<'a>(&'a mut self, ctx: &mut Context<'a, '_>) { // If this hits, you forgot to block_end() somewhere. The best way to figure // out where is to do a binary search of commenting out code in main.rs. debug_assert!( ctx.tree.current_node.borrow().stack_parent.is_none(), "Dangling parent! Did you miss a block_end?" ); // End the root node. ctx.block_end(); // Ensure that focus doesn't escape the active modal. if let Some(node) = ctx.last_modal && !self.is_subtree_focused(&node.borrow()) { ctx.steal_focus_for(node); } // If nodes have appeared or disappeared, we need to re-render. // Same, if the focus has changed (= changes the highlight color, etc.). let mut needs_settling = ctx.needs_settling; needs_settling |= self.prev_tree.checksum != ctx.tree.checksum; // Adopt the new tree and recalculate the node hashmap. // // SAFETY: The memory used by the tree is owned by the `self.arena_next` right now. // Stealing the tree here thus doesn't need to copy any memory unless someone resets the arena. // (The arena is reset in `reset()` above.) unsafe { self.prev_tree = mem::transmute_copy(&ctx.tree); self.prev_node_map = NodeMap::new(mem::transmute(&self.arena_next), &self.prev_tree); } let mut focus_path_pop_min = 0; // If the user pressed Escape, we move the focus to a parent node. if !ctx.input_consumed && ctx.consume_shortcut(vk::ESCAPE) { focus_path_pop_min = 1; } // Remove any unknown nodes from the focus path. // It's important that we do this after the tree has been swapped out, // so that pop_focusable_node() has access to the newest version of the tree. needs_settling |= self.pop_focusable_node(focus_path_pop_min); // `needs_more_settling()` depends on the current value // of `settling_have` and so we increment it first. self.settling_have += 1; if needs_settling { self.needs_more_settling(); } // Remove cached text editors that are no longer in use. self.cached_text_buffers.retain(|c| c.seen); for root in Tree::iterate_siblings(Some(self.prev_tree.root_first)) { let mut root = root.borrow_mut(); root.compute_intrinsic_size(unsafe { mem::transmute(&self.arena_next) }); } let viewport = self.size.as_rect(); for root in Tree::iterate_siblings(Some(self.prev_tree.root_first)) { let mut root = root.borrow_mut(); let root = &mut *root; if let Some(float) = &root.attributes.float { let mut x = 0; let mut y = 0; if let Some(node) = root.parent { let node = node.borrow(); x = node.outer.left; y = node.outer.top; } let size = root.intrinsic_to_outer(); x += (float.offset_x - float.gravity_x * size.width as f32) as CoordType; y += (float.offset_y - float.gravity_y * size.height as f32) as CoordType; root.outer.left = x; root.outer.top = y; root.outer.right = x + size.width; root.outer.bottom = y + size.height; root.outer = root.outer.intersect(viewport); } else { root.outer = viewport; } root.inner = root.outer_to_inner(root.outer); root.outer_clipped = root.outer; root.inner_clipped = root.inner; let outer = root.outer; root.layout_children(outer); } } fn build_node_path(node: Option<&NodeCell>, path: &mut Vec) { path.clear(); if let Some(mut node) = node { loop { let n = node.borrow(); path.push(n.id); node = match n.parent { Some(parent) => parent, None => break, }; } path.reverse(); } else { path.push(ROOT_ID); } } fn clean_node_path(path: &mut Vec) { Self::build_node_path(None, path); } /// After you finished processing all input, continue redrawing your UI until this returns false. pub fn needs_settling(&mut self) -> bool { self.settling_have <= self.settling_want } fn needs_more_settling(&mut self) { // If the focus has changed, the new node may need to be re-rendered. // Same, every time we encounter a previously unknown node via `get_prev_node`, // because that means it likely failed to get crucial information such as the layout size. debug_assert!(self.settling_have <= 15); self.settling_want = (self.settling_have + 1).min(20); } /// Renders the last frame into the framebuffer and returns the VT output. pub fn render<'a>(&mut self, arena: &'a Arena) -> BString<'a> { self.framebuffer.flip(self.size); for child in self.prev_tree.iterate_roots() { let mut child = child.borrow_mut(); self.render_node(&mut child); } self.framebuffer.render(arena) } /// Recursively renders each node and its children. #[allow(clippy::only_used_in_recursion)] fn render_node(&mut self, node: &mut Node) { let outer_clipped = node.outer_clipped; if outer_clipped.is_empty() { return; } if node.attributes.bordered { // ┌────┐ { let scratch = scratch_arena(None); let mut fill = BString::empty(); fill.push(&*scratch, '┌'); fill.push_repeat( &*scratch, '─', (outer_clipped.right - outer_clipped.left - 2) as usize, ); fill.push(&*scratch, '┐'); self.framebuffer.replace_text( outer_clipped.top, outer_clipped.left, outer_clipped.right, &fill, ); } // │ │ { let scratch = scratch_arena(None); let mut fill = BString::empty(); fill.push(&*scratch, '│'); fill.push_repeat( &*scratch, ' ', (outer_clipped.right - outer_clipped.left - 2) as usize, ); fill.push(&*scratch, '│'); for y in outer_clipped.top + 1..outer_clipped.bottom - 1 { self.framebuffer.replace_text( y, outer_clipped.left, outer_clipped.right, &fill, ); } } // └────┘ { let scratch = scratch_arena(None); let mut fill = BString::empty(); fill.push(&*scratch, '└'); fill.push_repeat( &*scratch, '─', (outer_clipped.right - outer_clipped.left - 2) as usize, ); fill.push(&*scratch, '┘'); self.framebuffer.replace_text( outer_clipped.bottom - 1, outer_clipped.left, outer_clipped.right, &fill, ); } } if node.attributes.float.is_some() { if !node.attributes.bordered { let scratch = scratch_arena(None); let mut fill = BString::empty(); fill.push_repeat( &*scratch, ' ', (outer_clipped.right - outer_clipped.left) as usize, ); for y in outer_clipped.top..outer_clipped.bottom { self.framebuffer.replace_text( y, outer_clipped.left, outer_clipped.right, &fill, ); } } self.framebuffer.replace_attr(outer_clipped, Attributes::All, Attributes::None); if matches!(node.content, NodeContent::Modal(_)) { let rect = Rect { left: 0, top: 0, right: self.size.width, bottom: self.size.height }; let dim = self.indexed_alpha(IndexedColor::Background, 1, 2); self.framebuffer.blend_bg(rect, dim); self.framebuffer.blend_fg(rect, dim); } } self.framebuffer.blend_bg(outer_clipped, node.attributes.bg); self.framebuffer.blend_fg(outer_clipped, node.attributes.fg); if node.attributes.reverse { self.framebuffer.reverse(outer_clipped); } let inner = node.inner; let inner_clipped = node.inner_clipped; if inner_clipped.is_empty() { return; } match &mut node.content { NodeContent::Modal(title) if !title.is_empty() => { self.framebuffer.replace_text( node.outer.top, node.outer.left + 2, node.outer.right - 1, title, ); } NodeContent::Text(content) => self.render_styled_text( inner, node.intrinsic_size.width, &content.text, &content.chunks, content.overflow, ), NodeContent::Textarea(tc) => { let mut tb = tc.buffer.borrow_mut(); let mut destination = Rect { left: inner_clipped.left, top: inner_clipped.top, right: inner_clipped.right, bottom: inner_clipped.bottom, }; if !tc.single_line { // Account for the scrollbar. destination.right -= 1; } if let Some(res) = tb.render(tc.scroll_offset, destination, tc.has_focus, &mut self.framebuffer) { tc.scroll_offset_x_max = res.visual_pos_x_max; } if !tc.single_line { // Render the scrollbar. let track = Rect { left: inner_clipped.right - 1, top: inner_clipped.top, right: inner_clipped.right, bottom: inner_clipped.bottom, }; tc.thumb_height = self.framebuffer.draw_scrollbar( inner_clipped, track, tc.scroll_offset.y, tb.visual_line_count() + inner.height() - 1, ); } } NodeContent::Scrollarea(sc) => { let content = node.children.first.unwrap().borrow(); let track = Rect { left: inner.right, top: inner.top, right: inner.right + 1, bottom: inner.bottom, }; sc.thumb_height = self.framebuffer.draw_scrollbar( outer_clipped, track, sc.scroll_offset.y, content.intrinsic_size.height, ); } _ => {} } for child in Tree::iterate_siblings(node.children.first) { let mut child = child.borrow_mut(); self.render_node(&mut child); } } fn render_styled_text( &mut self, target: Rect, actual_width: CoordType, text: &str, chunks: &[StyledTextChunk], overflow: Overflow, ) { let target_width = target.width(); // The section of `text` that is skipped by the ellipsis. let mut skipped = 0..0; // The number of columns skipped by the ellipsis. let mut skipped_cols = 0; if overflow == Overflow::Clip || target_width >= actual_width { self.framebuffer.replace_text(target.top, target.left, target.right, text); } else { let bytes = text.as_bytes(); let mut cfg = unicode::MeasurementConfig::new(&bytes); match overflow { Overflow::Clip => unreachable!(), Overflow::TruncateHead => { let beg = cfg.goto_visual(Point { x: actual_width - target_width + 1, y: 0 }); skipped = 0..beg.offset; skipped_cols = beg.visual_pos.x - 1; } Overflow::TruncateMiddle => { let mid_beg_x = (target_width - 1) / 2; let mid_end_x = actual_width - target_width / 2; let beg = cfg.goto_visual(Point { x: mid_beg_x, y: 0 }); let end = cfg.goto_visual(Point { x: mid_end_x, y: 0 }); skipped = beg.offset..end.offset; skipped_cols = end.visual_pos.x - beg.visual_pos.x - 1; } Overflow::TruncateTail => { let end = cfg.goto_visual(Point { x: target_width - 1, y: 0 }); skipped_cols = actual_width - end.visual_pos.x - 1; skipped = end.offset..text.len(); } } let scratch = scratch_arena(None); let mut modified = BString::empty(); modified.reserve(&*scratch, text.len() + 3); modified.push_str(&*scratch, &text[..skipped.start]); modified.push(&*scratch, '…'); modified.push_str(&*scratch, &text[skipped.end..]); self.framebuffer.replace_text(target.top, target.left, target.right, &modified); } if !chunks.is_empty() { let bytes = text.as_bytes(); let mut cfg = unicode::MeasurementConfig::new(&bytes).with_cursor(unicode::Cursor { visual_pos: Point { x: target.left, y: 0 }, ..Default::default() }); let mut iter = chunks.iter().peekable(); while let Some(chunk) = iter.next() { let beg = chunk.offset; let end = iter.peek().map_or(text.len(), |c| c.offset); if beg >= skipped.start && end <= skipped.end { // Chunk is fully inside the text skipped by the ellipsis. // We don't need to render it at all. continue; } if beg < skipped.start { let beg = cfg.goto_offset(beg).visual_pos.x; let end = cfg.goto_offset(end.min(skipped.start)).visual_pos.x; let rect = Rect { left: beg, top: target.top, right: end, bottom: target.bottom }; self.framebuffer.blend_fg(rect, chunk.fg); self.framebuffer.replace_attr(rect, chunk.attr, chunk.attr); } if end > skipped.end { let beg = cfg.goto_offset(beg.max(skipped.end)).visual_pos.x - skipped_cols; let end = cfg.goto_offset(end).visual_pos.x - skipped_cols; let rect = Rect { left: beg, top: target.top, right: end, bottom: target.bottom }; self.framebuffer.blend_fg(rect, chunk.fg); self.framebuffer.replace_attr(rect, chunk.attr, chunk.attr); } } } } /// Outputs a debug string of the layout and focus tree. pub fn debug_layout<'a>(&mut self, arena: &'a Arena) -> BString<'a> { let mut result = BString::empty(); result.push_str(arena, "general:\r\n- focus_path:\r\n"); for &id in &self.focused_node_path { arena_write_fmt!(arena, result, " - {id:016x}\r\n"); } result.push_str(arena, "\r\ntree:\r\n"); for root in self.prev_tree.iterate_roots() { Tree::visit_all(root, root, true, |node| { let node = node.borrow(); let depth = node.depth; result.push_repeat(arena, ' ', depth * 2); arena_write_fmt!(arena, result, "- id: {:016x}\r\n", node.id); result.push_repeat(arena, ' ', depth * 2); arena_write_fmt!(arena, result, " classname: {}\r\n", node.classname); if depth == 0 && let Some(parent) = node.parent { let parent = parent.borrow(); result.push_repeat(arena, ' ', depth * 2); arena_write_fmt!(arena, result, " parent: {:016x}\r\n", parent.id); } result.push_repeat(arena, ' ', depth * 2); arena_write_fmt!( arena, result, " intrinsic: {{{}, {}}}\r\n", node.intrinsic_size.width, node.intrinsic_size.height ); result.push_repeat(arena, ' ', depth * 2); arena_write_fmt!( arena, result, " outer: {{{}, {}, {}, {}}}\r\n", node.outer.left, node.outer.top, node.outer.right, node.outer.bottom ); result.push_repeat(arena, ' ', depth * 2); arena_write_fmt!( arena, result, " inner: {{{}, {}, {}, {}}}\r\n", node.inner.left, node.inner.top, node.inner.right, node.inner.bottom ); if node.attributes.bordered { result.push_repeat(arena, ' ', depth * 2); result.push_str(arena, " bordered: true\r\n"); } if node.attributes.bg.to_ne() != 0 { result.push_repeat(arena, ' ', depth * 2); arena_write_fmt!(arena, result, " bg: {:?}\r\n", node.attributes.bg); } if node.attributes.fg.to_ne() != 0 { result.push_repeat(arena, ' ', depth * 2); arena_write_fmt!(arena, result, " fg: {:?}\r\n", node.attributes.fg); } if self.is_node_focused(node.id) { result.push_repeat(arena, ' ', depth * 2); result.push_str(arena, " focused: true\r\n"); } match &node.content { NodeContent::Text(content) => { result.push_repeat(arena, ' ', depth * 2); arena_write_fmt!( arena, result, " text: \"{}\"\r\n", &content.text ); } NodeContent::Textarea(content) => { let tb = content.buffer.borrow(); let tb = &*tb; result.push_repeat(arena, ' ', depth * 2); arena_write_fmt!(arena, result, " textarea: {tb:p}\r\n"); } NodeContent::Scrollarea(..) => { result.push_repeat(arena, ' ', depth * 2); result.push_str(arena, " scrollable: true\r\n"); } _ => {} } VisitControl::Continue }); } result } fn was_mouse_down_on_node(&self, id: u64) -> bool { self.mouse_down_node_path.last() == Some(&id) } fn was_mouse_down_on_subtree(&self, node: &Node) -> bool { self.mouse_down_node_path.get(node.depth) == Some(&node.id) } fn is_node_focused(&self, id: u64) -> bool { // We construct the focused_node_path always with at least 1 element (the root id). unsafe { *self.focused_node_path.last().unwrap_unchecked() == id } } fn is_subtree_focused(&self, node: &Node) -> bool { self.focused_node_path.get(node.depth) == Some(&node.id) } fn is_subtree_focused_alt(&self, id: u64, depth: usize) -> bool { self.focused_node_path.get(depth) == Some(&id) } fn pop_focusable_node(&mut self, pop_minimum: usize) -> bool { let last_before = self.focused_node_path.last().cloned().unwrap_or(0); // Remove `pop_minimum`-many nodes from the end of the focus path. let path = &self.focused_node_path[..]; let path = &path[..path.len().saturating_sub(pop_minimum)]; let mut len = 0; for (i, &id) in path.iter().enumerate() { // Truncate the path so that it only contains nodes that still exist. let Some(node) = self.prev_node_map.get(id) else { break; }; let n = node.borrow(); // If the caller requested upward movement, pop out of the current focus void, if any. // This is kind of janky, to be fair. if pop_minimum != 0 && n.attributes.focus_void { break; } // Skip over those that aren't focusable. if n.attributes.focusable { // At this point `n.depth == i` should be true, // but I kind of don't want to rely on that. len = i + 1; } } self.focused_node_path.truncate(len); // If it's empty now, push `ROOT_ID` because there must always be >=1 element. if self.focused_node_path.is_empty() { self.focused_node_path.push(ROOT_ID); } // Return true if the focus path changed. let last_after = self.focused_node_path.last().cloned().unwrap_or(0); last_before != last_after } // Scroll the focused node(s) into view inside scrollviews fn scroll_to_focused(&mut self) -> bool { let focused_id = self.focused_node_path.last().cloned().unwrap_or(0); if self.focused_node_for_scrolling == focused_id { return false; } let Some(node) = self.prev_node_map.get(focused_id) else { // Node not found because we're using the old layout tree. // Retry in the next rendering loop. return true; }; let mut node = node.borrow_mut(); let mut scroll_to = node.outer; while node.parent.is_some() && node.attributes.float.is_none() { let n = &mut *node; if let NodeContent::Scrollarea(sc) = &mut n.content { let off_y = sc.scroll_offset.y.max(0); let mut y = off_y; y = y.min(scroll_to.top - n.inner.top + off_y); y = y.max(scroll_to.bottom - n.inner.bottom + off_y); sc.scroll_offset.y = y; scroll_to = n.outer; } node = node.parent.unwrap().borrow_mut(); } self.focused_node_for_scrolling = focused_id; true } } /// Context is a temporary object that is created for each frame. /// Its primary purpose is to build a UI tree. pub struct Context<'a, 'input> { tui: &'a mut Tui, /// Current text input, if any. input_text: Option<&'input str>, /// Current keyboard input, if any. input_keyboard: Option, input_mouse_modifiers: InputKeyMod, input_mouse_click: CoordType, /// By how much the mouse wheel was scrolled since the last frame. input_scroll_delta: Point, input_consumed: bool, tree: Tree<'a>, last_modal: Option<&'a NodeCell<'a>>, focused_node: Option<&'a NodeCell<'a>>, next_block_id_mixin: u64, needs_settling: bool, #[cfg(debug_assertions)] seen_ids: HashSet, } impl<'a> Drop for Context<'a, '_> { fn drop(&mut self) { let tui: &'a mut Tui = unsafe { mem::transmute(&mut *self.tui) }; tui.report_context_completion(self); } } impl<'a> Context<'a, '_> { /// Get an arena for temporary allocations such as for [`arena_format`]. pub fn arena(&self) -> &'a Arena { // TODO: // `Context` borrows `Tui` for lifetime 'a, so `self.tui` should be `&'a Tui`, right? // And if I do `&self.tui.arena` then that should be 'a too, right? // Searching for and failing to find a workaround for this was _very_ annoying. // // SAFETY: Both the returned reference and its allocations outlive &self. unsafe { mem::transmute::<&'_ Arena, &'a Arena>(&self.tui.arena_next) } } /// Returns the viewport size. pub fn size(&self) -> Size { self.tui.size() } /// Returns an indexed color from the framebuffer. #[inline] pub fn indexed(&self, index: IndexedColor) -> StraightRgba { self.tui.framebuffer.indexed(index) } /// Returns an indexed color from the framebuffer with the given alpha. /// See [`Framebuffer::indexed_alpha()`]. #[inline] pub fn indexed_alpha( &self, index: IndexedColor, numerator: u32, denominator: u32, ) -> StraightRgba { self.tui.framebuffer.indexed_alpha(index, numerator, denominator) } /// Returns a color in contrast with the given color. /// See [`Framebuffer::contrasted()`]. pub fn contrasted(&self, color: StraightRgba) -> StraightRgba { self.tui.framebuffer.contrasted(color) } /// Returns the clipboard. pub fn clipboard_ref(&self) -> &Clipboard { &self.tui.clipboard } /// Returns the clipboard (mutable). pub fn clipboard_mut(&mut self) -> &mut Clipboard { &mut self.tui.clipboard } /// Tell the UI framework that your state changed and you need another layout pass. pub fn needs_rerender(&mut self) { // If this hits, the call stack is responsible is trying to deadlock you. debug_assert!(self.tui.settling_have < 15); self.needs_settling = true; } /// Begins a generic UI block (container) with a unique ID derived from the given `classname`. pub fn block_begin(&mut self, classname: &'static str) { let parent = self.tree.current_node; let mut id = hash_str(parent.borrow().id, classname); if self.next_block_id_mixin != 0 { id = hash(id, &self.next_block_id_mixin.to_ne_bytes()); self.next_block_id_mixin = 0; } // If this hits, you have tried to create a block with the same ID as a previous one // somewhere up this call stack. Change the classname, or use next_block_id_mixin(). // TODO: HashMap #[cfg(debug_assertions)] if !self.seen_ids.insert(id) { panic!("Duplicate node ID: {id:x}"); } let node = Tree::alloc_node(self.arena()); { let mut n = node.borrow_mut(); n.id = id; n.classname = classname; } self.tree.push_child(node); } /// Ends the current UI block, returning to its parent container. pub fn block_end(&mut self) { self.tree.pop_stack(); self.block_end_move_focus(); } fn block_end_move_focus(&mut self) { // At this point, it's more like "focus_well?" instead of "focus_well!". let focus_well = self.tree.last_node; // Remember the focused node, if any, because once the code below runs, // we need it for the `Tree::visit_all` call. if self.is_focused() { self.focused_node = Some(focus_well); } // The mere fact that there's a `focused_node` indicates that we're the // first `block_end()` call that's a focus well and also contains the focus. let Some(focused) = self.focused_node else { return; }; // Filter down to nodes that are focus wells and contain the focus. They're // basically the "tab container". We test for the node depth to ensure that // we don't accidentally pick a focus well next to or inside the focused node. { let n = focus_well.borrow(); if !n.attributes.focus_well || n.depth > focused.borrow().depth { return; } } // Filter down to Tab/Shift+Tab inputs. if self.input_consumed { return; } let Some(input) = self.input_keyboard else { return; }; if !matches!(input, SHIFT_TAB | vk::TAB) { return; } let forward = input == vk::TAB; let mut focused_start = focused; let mut focused_next = focused; // We may be in a focus void right now (= doesn't want to be tabbed into), // so first we must go up the tree until we're outside of it. loop { if ptr::eq(focused_start, focus_well) { // If we hit the root / focus well, we weren't in a focus void, // and can reset `focused_before` to the current focused node. focused_start = focused; break; } focused_start = focused_start.borrow().parent.unwrap(); if focused_start.borrow().attributes.focus_void { break; } } Tree::visit_all(focus_well, focused_start, forward, |node| { let n = node.borrow(); if n.attributes.focusable && !ptr::eq(node, focused_start) { focused_next = node; VisitControl::Stop } else if n.attributes.focus_void { VisitControl::SkipChildren } else { VisitControl::Continue } }); if ptr::eq(focused_next, focused_start) { return; } Tui::build_node_path(Some(focused_next), &mut self.tui.focused_node_path); self.set_input_consumed(); self.needs_rerender(); } /// Mixes in an extra value to the next UI block's ID for uniqueness. /// Use this when you build a list of items with the same classname. pub fn next_block_id_mixin(&mut self, id: u64) { self.next_block_id_mixin = id; } fn attr_focusable(&mut self) { let mut last_node = self.tree.last_node.borrow_mut(); last_node.attributes.focusable = true; } /// If this is the first time the current node is being drawn, /// it'll steal the active focus. pub fn focus_on_first_present(&mut self) { let steal = { let mut last_node = self.tree.last_node.borrow_mut(); last_node.attributes.focusable = true; self.tui.prev_node_map.get(last_node.id).is_none() }; if steal { self.steal_focus(); } } /// Steals the focus unconditionally. pub fn steal_focus(&mut self) { self.steal_focus_for(self.tree.last_node); } fn steal_focus_for(&mut self, node: &NodeCell<'a>) { if !self.tui.is_node_focused(node.borrow().id) { Tui::build_node_path(Some(node), &mut self.tui.focused_node_path); self.needs_rerender(); } } /// If the current node owns the focus, it'll be given to the parent. pub fn toss_focus_up(&mut self) { if self.tui.pop_focusable_node(1) { self.needs_rerender(); } } /// If the parent node owns the focus, it'll be given to the current node. pub fn inherit_focus(&mut self) { let mut last_node = self.tree.last_node.borrow_mut(); let Some(parent) = last_node.parent else { return; }; last_node.attributes.focusable = true; // Mark the parent as focusable, so that if the user presses Escape, // and `block_end` bubbles the focus up the tree, it'll stop on our parent, // which will then focus us on the next iteration. let mut parent = parent.borrow_mut(); parent.attributes.focusable = true; if self.tui.is_node_focused(parent.id) { self.needs_rerender(); self.tui.focused_node_path.push(last_node.id); } } /// Causes keyboard focus to be unable to escape this node and its children. /// It's a "well" because if the focus is inside it, it can't escape. pub fn attr_focus_well(&mut self) { let mut last_node = self.tree.last_node.borrow_mut(); last_node.attributes.focus_well = true; } /// Explicitly sets the intrinsic size of the current node. /// The intrinsic size is the size the node ideally wants to be. pub fn attr_intrinsic_size(&mut self, size: Size) { let mut last_node = self.tree.last_node.borrow_mut(); last_node.intrinsic_size = size; last_node.intrinsic_size_set = true; } /// Turns the current node into a floating node, /// like a popup, modal or a tooltip. pub fn attr_float(&mut self, spec: FloatSpec) { let last_node = self.tree.last_node; let anchor = { let ln = last_node.borrow(); match spec.anchor { Anchor::Last if ln.siblings.prev.is_some() => ln.siblings.prev, Anchor::Last | Anchor::Parent => ln.parent, // By not giving such floats a parent, they get the same origin as the original root node, // but they also gain their own "root id" in the tree. That way, their focus path is totally unique, // which means that we can easily check if a modal is open by calling `is_focused()` on the original root. Anchor::Root => None, } }; self.tree.move_node_to_root(last_node, anchor); let mut ln = last_node.borrow_mut(); ln.attributes.focus_well = true; ln.attributes.float = Some(FloatAttributes { gravity_x: spec.gravity_x.clamp(0.0, 1.0), gravity_y: spec.gravity_y.clamp(0.0, 1.0), offset_x: spec.offset_x, offset_y: spec.offset_y, }); ln.attributes.bg = self.tui.floater_default_bg; ln.attributes.fg = self.tui.floater_default_fg; } /// Gives the current node a border. pub fn attr_border(&mut self) { let mut last_node = self.tree.last_node.borrow_mut(); last_node.attributes.bordered = true; } /// Sets the current node's position inside the parent. pub fn attr_position(&mut self, align: Position) { let mut last_node = self.tree.last_node.borrow_mut(); last_node.attributes.position = align; } /// Assigns padding to the current node. pub fn attr_padding(&mut self, padding: Rect) { let mut last_node = self.tree.last_node.borrow_mut(); last_node.attributes.padding = Self::normalize_rect(padding); } fn normalize_rect(rect: Rect) -> Rect { Rect { left: rect.left.max(0), top: rect.top.max(0), right: rect.right.max(0), bottom: rect.bottom.max(0), } } /// Assigns a sRGB background color to the current node. pub fn attr_background_rgba(&mut self, bg: StraightRgba) { let mut last_node = self.tree.last_node.borrow_mut(); last_node.attributes.bg = bg; } /// Assigns a sRGB foreground color to the current node. pub fn attr_foreground_rgba(&mut self, fg: StraightRgba) { let mut last_node = self.tree.last_node.borrow_mut(); last_node.attributes.fg = fg; } /// Applies reverse-video to the current node: /// Background and foreground colors are swapped. pub fn attr_reverse(&mut self) { let mut last_node = self.tree.last_node.borrow_mut(); last_node.attributes.reverse = true; } /// Checks if the current keyboard input matches the given shortcut, /// consumes it if it is and returns true in that case. pub fn consume_shortcut(&mut self, shortcut: InputKey) -> bool { if !self.input_consumed && self.input_keyboard == Some(shortcut) { self.set_input_consumed(); true } else { false } } /// Returns current keyboard input, if any. /// Returns None if the input was already consumed. pub fn keyboard_input(&self) -> Option { if self.input_consumed { None } else { self.input_keyboard } } #[inline] pub fn set_input_consumed(&mut self) { debug_assert!(!self.input_consumed); self.set_input_consumed_unchecked(); } #[inline] fn set_input_consumed_unchecked(&mut self) { self.input_consumed = true; } /// Returns whether the mouse was pressed down on the current node. pub fn was_mouse_down(&mut self) -> bool { let last_node = self.tree.last_node.borrow(); self.tui.was_mouse_down_on_node(last_node.id) } /// Returns whether the mouse was pressed down on the current node's subtree. pub fn contains_mouse_down(&mut self) -> bool { let last_node = self.tree.last_node.borrow(); self.tui.was_mouse_down_on_subtree(&last_node) } /// Returns whether the current node is focused. pub fn is_focused(&mut self) -> bool { let last_node = self.tree.last_node.borrow(); self.tui.is_node_focused(last_node.id) } /// Returns whether the current node's subtree is focused. pub fn contains_focus(&mut self) -> bool { let last_node = self.tree.last_node.borrow(); self.tui.is_subtree_focused(&last_node) } /// Begins a modal window. Call [`Context::modal_end()`]. pub fn modal_begin(&mut self, classname: &'static str, title: &str) { self.block_begin(classname); self.attr_float(FloatSpec { anchor: Anchor::Root, gravity_x: 0.5, gravity_y: 0.5, offset_x: self.tui.size.width as f32 * 0.5, offset_y: self.tui.size.height as f32 * 0.5, }); self.attr_border(); self.attr_background_rgba(self.tui.modal_default_bg); self.attr_foreground_rgba(self.tui.modal_default_fg); self.attr_focus_well(); self.focus_on_first_present(); let mut last_node = self.tree.last_node.borrow_mut(); let title = if title.is_empty() { BString::empty() } else { arena_format!(self.arena(), " {} ", title) }; last_node.content = NodeContent::Modal(title); self.last_modal = Some(self.tree.last_node); } /// Ends the current modal window block. /// Returns true if the user pressed Escape (a request to close). pub fn modal_end(&mut self) -> bool { self.block_end(); // Consume the input unconditionally, so that the root (the "main window") // doesn't accidentally receive any input via `consume_shortcut()`. if self.contains_focus() { let exit = !self.input_consumed && self.input_keyboard == Some(vk::ESCAPE); self.set_input_consumed_unchecked(); exit } else { false } } /// Begins a table block. Call [`Context::table_end()`]. /// Tables are the primary way to create a grid layout, /// and to layout controls on a single row (= a table with 1 row). pub fn table_begin(&mut self, classname: &'static str) { self.block_begin(classname); let mut last_node = self.tree.last_node.borrow_mut(); last_node.content = NodeContent::Table(TableContent { columns: BVec::empty(), cell_gap: Default::default(), }); } /// Assigns widths to the columns of the current table. /// By default, the table will left-align all columns. pub fn table_set_columns(&mut self, columns: &[CoordType]) { let mut last_node = self.tree.last_node.borrow_mut(); if let NodeContent::Table(spec) = &mut last_node.content { spec.columns.clear(); spec.columns.extend_from_slice(self.arena(), columns); } else { debug_assert!(false); } } /// Assigns the gap between cells in the current table. pub fn table_set_cell_gap(&mut self, cell_gap: Size) { let mut last_node = self.tree.last_node.borrow_mut(); if let NodeContent::Table(spec) = &mut last_node.content { spec.cell_gap = cell_gap; } else { debug_assert!(false); } } /// Starts the next row in the current table. pub fn table_next_row(&mut self) { { let current_node = self.tree.current_node.borrow(); // If this is the first call to table_next_row() inside a new table, the // current_node will refer to the table. Otherwise, it'll refer to the current row. if !matches!(current_node.content, NodeContent::Table(_)) { let Some(parent) = current_node.parent else { return; }; let parent = parent.borrow(); // Neither the current nor its parent nodes are a table? // You definitely called this outside of a table block. debug_assert!(matches!(parent.content, NodeContent::Table(_))); self.block_end(); self.table_end_row(); self.next_block_id_mixin(parent.child_count as u64); } } self.block_begin("row"); } fn table_end_row(&mut self) { self.table_move_focus(vk::LEFT, vk::RIGHT); } /// Ends the current table block. pub fn table_end(&mut self) { let current_node = self.tree.current_node.borrow(); // If this is the first call to table_next_row() inside a new table, the // current_node will refer to the table. Otherwise, it'll refer to the current row. if !matches!(current_node.content, NodeContent::Table(_)) { self.block_end(); self.table_end_row(); } self.block_end(); // table self.table_move_focus(vk::UP, vk::DOWN); } fn table_move_focus(&mut self, prev_key: InputKey, next_key: InputKey) { // Filter down to table rows that are focused. if !self.contains_focus() { return; } // Filter down to our prev/next inputs. if self.input_consumed { return; } let Some(input) = self.input_keyboard else { return; }; if input != prev_key && input != next_key { return; } let container = self.tree.last_node; let Some(&focused_id) = self.tui.focused_node_path.get(container.borrow().depth + 1) else { return; }; let mut prev_next = NodeSiblings { prev: None, next: None }; let mut focused = None; // Iterate through the cells in the row / the rows in the table, looking for focused_id. // Take note of the previous and next focusable cells / rows around the focused one. for cell in Tree::iterate_siblings(container.borrow().children.first) { let n = cell.borrow(); if n.id == focused_id { focused = Some(cell); } else if n.attributes.focusable { if focused.is_none() { prev_next.prev = Some(cell); } else { prev_next.next = Some(cell); break; } } } if focused.is_none() { return; } let forward = input == next_key; let children_idx = if forward { NodeChildren::FIRST } else { NodeChildren::LAST }; let siblings_idx = if forward { NodeSiblings::NEXT } else { NodeSiblings::PREV }; let Some(focused_next) = prev_next.get(siblings_idx).or_else(|| container.borrow().children.get(children_idx)) else { return; }; Tui::build_node_path(Some(focused_next), &mut self.tui.focused_node_path); self.set_input_consumed(); self.needs_rerender(); } /// Creates a simple text label. pub fn label(&mut self, classname: &'static str, text: &str) { self.styled_label_begin(classname); self.styled_label_add_text(text); self.styled_label_end(); } /// Creates a styled text label. /// /// # Example /// ``` /// use edit::framebuffer::IndexedColor; /// use edit::tui::Context; /// /// fn draw(ctx: &mut Context) { /// ctx.styled_label_begin("label"); /// // Shows "Hello" in the inherited foreground color. /// ctx.styled_label_add_text("Hello"); /// // Shows ", World!" next to "Hello" in red. /// ctx.styled_label_set_foreground(ctx.indexed(IndexedColor::Red)); /// ctx.styled_label_add_text(", World!"); /// } /// ``` pub fn styled_label_begin(&mut self, classname: &'static str) { self.block_begin(classname); self.tree.last_node.borrow_mut().content = NodeContent::Text(TextContent { text: BString::empty(), chunks: BVec::empty(), overflow: Overflow::Clip, }); } /// Changes the active pencil color of the current label. pub fn styled_label_set_foreground(&mut self, fg: StraightRgba) { let mut node = self.tree.last_node.borrow_mut(); let NodeContent::Text(content) = &mut node.content else { unreachable!(); }; let last = content.chunks.last().unwrap_or(&INVALID_STYLED_TEXT_CHUNK); if last.offset != content.text.len() && last.fg != fg { content.chunks.push( self.arena(), StyledTextChunk { offset: content.text.len(), fg, attr: last.attr }, ); } } /// Changes the active pencil attributes of the current label. pub fn styled_label_set_attributes(&mut self, attr: Attributes) { let mut node = self.tree.last_node.borrow_mut(); let NodeContent::Text(content) = &mut node.content else { unreachable!(); }; let last = content.chunks.last().unwrap_or(&INVALID_STYLED_TEXT_CHUNK); if last.offset != content.text.len() && last.attr != attr { content.chunks.push( self.arena(), StyledTextChunk { offset: content.text.len(), fg: last.fg, attr }, ); } } /// Adds text to the current label. pub fn styled_label_add_text(&mut self, text: &str) { let mut node = self.tree.last_node.borrow_mut(); let NodeContent::Text(content) = &mut node.content else { unreachable!(); }; content.text.push_str(self.arena(), text); } /// Ends the current label block. pub fn styled_label_end(&mut self) { { let mut last_node = self.tree.last_node.borrow_mut(); let NodeContent::Text(content) = &last_node.content else { return; }; let cursor = unicode::MeasurementConfig::new(&content.text.as_bytes()) .goto_visual(Point { x: CoordType::MAX, y: 0 }); last_node.intrinsic_size.width = cursor.visual_pos.x; last_node.intrinsic_size.height = 1; last_node.intrinsic_size_set = true; } self.block_end(); } /// Sets the overflow behavior of the current label. pub fn attr_overflow(&mut self, overflow: Overflow) { let mut last_node = self.tree.last_node.borrow_mut(); let NodeContent::Text(content) = &mut last_node.content else { return; }; content.overflow = overflow; } /// Creates a button with the given text. /// Returns true if the button was activated. pub fn button(&mut self, classname: &'static str, text: &str, style: ButtonStyle) -> bool { self.button_label(classname, text, style); self.attr_focusable(); if self.is_focused() { self.attr_reverse(); } self.button_activated() } /// Creates a checkbox with the given text. /// Returns true if the checkbox was activated. pub fn checkbox(&mut self, classname: &'static str, text: &str, checked: &mut bool) -> bool { self.styled_label_begin(classname); self.attr_focusable(); if self.is_focused() { self.attr_reverse(); } self.styled_label_add_text(if *checked { "[🗹 " } else { "[☐ " }); self.styled_label_add_text(text); self.styled_label_add_text("]"); self.styled_label_end(); let activated = self.button_activated(); if activated { *checked = !*checked; } activated } fn button_activated(&mut self) -> bool { if !self.input_consumed && ((self.input_mouse_click != 0 && self.contains_mouse_down()) || self.input_keyboard == Some(vk::RETURN) || self.input_keyboard == Some(vk::SPACE)) && self.is_focused() { self.set_input_consumed(); true } else { false } } /// Creates a text input field. /// Returns true if the text contents changed. pub fn editline(&mut self, classname: &'static str, text: &mut dyn WriteableDocument) -> bool { self.textarea_internal(classname, TextBufferPayload::Editline(text)) } /// Creates a text area. pub fn textarea(&mut self, classname: &'static str, tb: RcTextBuffer) { self.textarea_internal(classname, TextBufferPayload::Textarea(tb)); } fn textarea_internal(&mut self, classname: &'static str, payload: TextBufferPayload) -> bool { self.block_begin(classname); self.block_end(); let mut node = self.tree.last_node.borrow_mut(); let node = &mut *node; let single_line = match &payload { TextBufferPayload::Editline(_) => true, TextBufferPayload::Textarea(_) => false, }; let buffer = { let buffers = &mut self.tui.cached_text_buffers; let cached = match buffers.iter_mut().find(|t| t.node_id == node.id) { Some(cached) => { if let TextBufferPayload::Textarea(tb) = &payload { cached.editor = tb.clone(); }; cached.seen = true; cached } None => { // If the node is not in the cache, we need to create a new one. buffers.push(CachedTextBuffer { node_id: node.id, editor: match &payload { TextBufferPayload::Editline(_) => TextBuffer::new_rc(true).unwrap(), TextBufferPayload::Textarea(tb) => tb.clone(), }, seen: true, }); buffers.last_mut().unwrap() } }; // SAFETY: *Assuming* that there are no duplicate node IDs in the tree that // would cause this cache slot to be overwritten, then this operation is safe. // The text buffer cache will keep the buffer alive for us long enough. unsafe { mem::transmute(&*cached.editor) } }; node.content = NodeContent::Textarea(TextareaContent { buffer, scroll_offset: Default::default(), scroll_offset_y_drag_start: CoordType::MIN, scroll_offset_x_max: 0, thumb_height: 0, preferred_column: 0, single_line, has_focus: self.tui.is_node_focused(node.id), }); let content = match node.content { NodeContent::Textarea(ref mut content) => content, _ => unreachable!(), }; if let TextBufferPayload::Editline(text) = &payload { content.buffer.borrow_mut().copy_from_str(*text); } if let Some(node_prev) = self.tui.prev_node_map.get(node.id) { let node_prev = node_prev.borrow(); if let NodeContent::Textarea(content_prev) = &node_prev.content { content.scroll_offset = content_prev.scroll_offset; content.scroll_offset_y_drag_start = content_prev.scroll_offset_y_drag_start; content.scroll_offset_x_max = content_prev.scroll_offset_x_max; content.thumb_height = content_prev.thumb_height; content.preferred_column = content_prev.preferred_column; let mut text_width = node_prev.inner.width(); if !single_line { // Subtract -1 to account for the scrollbar. text_width -= 1; } let mut make_cursor_visible; { let mut tb = content.buffer.borrow_mut(); make_cursor_visible = tb.take_cursor_visibility_request(); make_cursor_visible |= tb.set_width(text_width); } make_cursor_visible |= self.textarea_handle_input(content, &node_prev, single_line); if make_cursor_visible { self.textarea_make_cursor_visible(content, &node_prev); } } else { debug_assert!(false); } } let dirty; { let mut tb = content.buffer.borrow_mut(); dirty = tb.is_dirty(); if dirty && let TextBufferPayload::Editline(text) = payload { tb.save_as_string(text); } } self.textarea_adjust_scroll_offset(content); if single_line { node.attributes.fg = self.indexed(IndexedColor::Foreground); node.attributes.bg = self.indexed(IndexedColor::Background); if !content.has_focus { node.attributes.fg = self.contrasted(node.attributes.bg); node.attributes.bg = self.indexed_alpha(IndexedColor::Background, 1, 2); } } node.attributes.focusable = true; node.intrinsic_size.height = content.buffer.borrow().visual_line_count(); node.intrinsic_size_set = true; dirty } fn textarea_handle_input( &mut self, tc: &mut TextareaContent, node_prev: &Node, single_line: bool, ) -> bool { if self.input_consumed { return false; } let mut tb = tc.buffer.borrow_mut(); let tb = &mut *tb; let mut make_cursor_visible = false; let mut change_preferred_column = false; // Scrolling works even if the node isn't focused. if self.input_scroll_delta != Point::default() && node_prev.inner_clipped.contains(self.tui.mouse_position) { tc.scroll_offset.x += self.input_scroll_delta.x; tc.scroll_offset.y += self.input_scroll_delta.y; self.set_input_consumed(); return make_cursor_visible; } else if self.tui.mouse_state != InputMouseState::None && self.tui.is_node_focused(node_prev.id) { let mouse = self.tui.mouse_position; let inner = node_prev.inner; let text_rect = Rect { left: inner.left + tb.margin_width(), top: inner.top, right: inner.right - !single_line as CoordType, bottom: inner.bottom, }; let track_rect = Rect { left: text_rect.right, top: inner.top, right: inner.right, bottom: inner.bottom, }; let pos = Point { x: mouse.x - inner.left - tb.margin_width() + tc.scroll_offset.x, y: mouse.y - inner.top + tc.scroll_offset.y, }; if text_rect.contains(self.tui.mouse_down_position) { if self.tui.mouse_is_drag { tb.selection_update_visual(pos); tc.preferred_column = tb.cursor_visual_pos().x; let height = inner.height(); // If the editor is only 1 line tall we can't possibly scroll up or down. if height >= 2 { fn calc(min: CoordType, max: CoordType, mouse: CoordType) -> CoordType { // Otherwise, the scroll zone is up to 3 lines at the top/bottom. let zone_height = ((max - min) / 2).min(3); // The .y positions where the scroll zones begin: // Mouse coordinates above top and below bottom respectively. let scroll_min = min + zone_height; let scroll_max = max - zone_height - 1; // Calculate the delta for scrolling up or down. let delta_min = (mouse - scroll_min).clamp(-zone_height, 0); let delta_max = (mouse - scroll_max).clamp(0, zone_height); // If I didn't mess up my logic here, only one of the two values can possibly be !=0. let idx = 3 + delta_min + delta_max; const SPEEDS: [CoordType; 7] = [-9, -3, -1, 0, 1, 3, 9]; let idx = idx.clamp(0, SPEEDS.len() as CoordType) as usize; SPEEDS[idx] } let delta_x = calc(text_rect.left, text_rect.right, mouse.x); let delta_y = calc(text_rect.top, text_rect.bottom, mouse.y); tc.scroll_offset.x += delta_x; tc.scroll_offset.y += delta_y; if delta_x != 0 || delta_y != 0 { self.tui.read_timeout = time::Duration::from_millis(25); } } } else { match self.input_mouse_click { 5.. => {} 4 => tb.select_all(), 3 => tb.select_line(), 2 => tb.select_word(), _ => match self.tui.mouse_state { InputMouseState::Left => { if self.input_mouse_modifiers.contains(kbmod::SHIFT) { // TODO: Untested because Windows Terminal surprisingly doesn't support Shift+Click. tb.selection_update_visual(pos); } else { tb.cursor_move_to_visual(pos); } tc.preferred_column = tb.cursor_visual_pos().x; make_cursor_visible = true; } _ => return false, }, } } } else if track_rect.contains(self.tui.mouse_down_position) { if self.tui.mouse_state == InputMouseState::Release { tc.scroll_offset_y_drag_start = CoordType::MIN; } else if self.tui.mouse_is_drag { if tc.scroll_offset_y_drag_start == CoordType::MIN { tc.scroll_offset_y_drag_start = tc.scroll_offset.y; } // The textarea supports 1 height worth of "scrolling beyond the end". // `track_height` is the same as the viewport height. let scrollable_height = tb.visual_line_count() - 1; if scrollable_height > 0 { let trackable = track_rect.height() - tc.thumb_height; let delta_y = mouse.y - self.tui.mouse_down_position.y; tc.scroll_offset.y = tc.scroll_offset_y_drag_start + (delta_y as i64 * scrollable_height as i64 / trackable as i64) as CoordType; } } } self.set_input_consumed(); return make_cursor_visible; } if !tc.has_focus { return false; } let mut write: &[u8] = &[]; if let Some(input) = &self.input_text { write = input.as_bytes(); } else if let Some(input) = &self.input_keyboard { let key = input.key(); let modifiers = input.modifiers(); make_cursor_visible = true; match key { vk::BACK => { let granularity = if modifiers == kbmod::CTRL { CursorMovement::Word } else { CursorMovement::Grapheme }; tb.delete(granularity, -1); } vk::TAB => { if single_line { // If this is just a simple input field, don't consume Tab (= early return). return false; } tb.indent_change(if modifiers == kbmod::SHIFT { -1 } else { 1 }); } vk::RETURN => { if single_line { // If this is just a simple input field, don't consume Enter (= early return). return false; } write = b"\n"; } vk::ESCAPE => { // If there was a selection, clear it and show the cursor (= fallthrough). if !tb.clear_selection() { if single_line { // If this is just a simple input field, don't consume the escape key // (early return) and don't show the cursor (= return false). return false; } // If this is a textarea, don't show the cursor if // the escape key was pressed and nothing happened. make_cursor_visible = false; } } vk::PRIOR => { let height = node_prev.inner.height() - 1; // If the cursor was already on the first line, // move it to the start of the buffer. if tb.cursor_visual_pos().y == 0 { tc.preferred_column = 0; } if modifiers == kbmod::SHIFT { tb.selection_update_visual(Point { x: tc.preferred_column, y: tb.cursor_visual_pos().y - height, }); } else { tb.cursor_move_to_visual(Point { x: tc.preferred_column, y: tb.cursor_visual_pos().y - height, }); } } vk::NEXT => { let height = node_prev.inner.height() - 1; // If the cursor was already on the last line, // move it to the end of the buffer. if tb.cursor_visual_pos().y >= tb.visual_line_count() - 1 { tc.preferred_column = CoordType::MAX; } if modifiers == kbmod::SHIFT { tb.selection_update_visual(Point { x: tc.preferred_column, y: tb.cursor_visual_pos().y + height, }); } else { tb.cursor_move_to_visual(Point { x: tc.preferred_column, y: tb.cursor_visual_pos().y + height, }); } if tc.preferred_column == CoordType::MAX { tc.preferred_column = tb.cursor_visual_pos().x; } } vk::END => { let logical_before = tb.cursor_logical_pos(); let destination = if modifiers.contains(kbmod::CTRL) { Point::MAX } else { Point { x: CoordType::MAX, y: tb.cursor_visual_pos().y } }; if modifiers.contains(kbmod::SHIFT) { tb.selection_update_visual(destination); } else { tb.cursor_move_to_visual(destination); } if !modifiers.contains(kbmod::CTRL) { let logical_after = tb.cursor_logical_pos(); // If word-wrap is enabled and the user presses End the first time, // it moves to the start of the visual line. The second time they // press it, it moves to the start of the logical line. if tb.is_word_wrap_enabled() && logical_after == logical_before { if modifiers == kbmod::SHIFT { tb.selection_update_logical(Point { x: CoordType::MAX, y: tb.cursor_logical_pos().y, }); } else { tb.cursor_move_to_logical(Point { x: CoordType::MAX, y: tb.cursor_logical_pos().y, }); } } } } vk::HOME => { let logical_before = tb.cursor_logical_pos(); let destination = if modifiers.contains(kbmod::CTRL) { Default::default() } else { Point { x: 0, y: tb.cursor_visual_pos().y } }; if modifiers.contains(kbmod::SHIFT) { tb.selection_update_visual(destination); } else { tb.cursor_move_to_visual(destination); } if !modifiers.contains(kbmod::CTRL) { let mut logical_after = tb.cursor_logical_pos(); // If word-wrap is enabled and the user presses Home the first time, // it moves to the start of the visual line. The second time they // press it, it moves to the start of the logical line. if tb.is_word_wrap_enabled() && logical_after == logical_before { if modifiers == kbmod::SHIFT { tb.selection_update_logical(Point { x: 0, y: tb.cursor_logical_pos().y, }); } else { tb.cursor_move_to_logical(Point { x: 0, y: tb.cursor_logical_pos().y, }); } logical_after = tb.cursor_logical_pos(); } // If the line has some indentation and the user pressed Home, // the first time it'll stop at the indentation. The second time // they press it, it'll move to the true start of the line. // // If the cursor is already at the start of the line, // we move it back to the end of the indentation. if logical_after.x == 0 && let indent_end = tb.indent_end_logical_pos() && (logical_before > indent_end || logical_before.x == 0) { if modifiers == kbmod::SHIFT { tb.selection_update_logical(indent_end); } else { tb.cursor_move_to_logical(indent_end); } } } } vk::LEFT => { let granularity = if modifiers.contains(KBMOD_FOR_WORD_NAV) { CursorMovement::Word } else { CursorMovement::Grapheme }; if modifiers.contains(kbmod::SHIFT) { tb.selection_update_delta(granularity, -1); } else if let Some((beg, _)) = tb.selection_range() { unsafe { tb.set_cursor(beg) }; } else { tb.cursor_move_delta(granularity, -1); } } vk::UP => { if single_line { return false; } match modifiers { kbmod::NONE => { let mut x = tc.preferred_column; let mut y = tb.cursor_visual_pos().y - 1; // If there's a selection we put the cursor above it. if let Some((beg, _)) = tb.selection_range() { x = beg.visual_pos.x; y = beg.visual_pos.y - 1; tc.preferred_column = x; } // If the cursor was already on the first line, // move it to the start of the buffer. if y < 0 { x = 0; tc.preferred_column = 0; } tb.cursor_move_to_visual(Point { x, y }); } kbmod::CTRL => { tc.scroll_offset.y -= 1; make_cursor_visible = false; } kbmod::SHIFT => { // If the cursor was already on the first line, // move it to the start of the buffer. if tb.cursor_visual_pos().y == 0 { tc.preferred_column = 0; } tb.selection_update_visual(Point { x: tc.preferred_column, y: tb.cursor_visual_pos().y - 1, }); } kbmod::ALT => tb.move_selected_lines(MoveLineDirection::Up), kbmod::CTRL_ALT => { // TODO: Add cursor above } _ => return false, } } vk::RIGHT => { let granularity = if modifiers.contains(KBMOD_FOR_WORD_NAV) { CursorMovement::Word } else { CursorMovement::Grapheme }; if modifiers.contains(kbmod::SHIFT) { tb.selection_update_delta(granularity, 1); } else if let Some((_, end)) = tb.selection_range() { unsafe { tb.set_cursor(end) }; } else { tb.cursor_move_delta(granularity, 1); } } vk::DOWN => { if single_line { return false; } match modifiers { kbmod::NONE => { let mut x = tc.preferred_column; let mut y = tb.cursor_visual_pos().y + 1; // If there's a selection we put the cursor below it. if let Some((_, end)) = tb.selection_range() { x = end.visual_pos.x; y = end.visual_pos.y + 1; tc.preferred_column = x; } // If the cursor was already on the last line, // move it to the end of the buffer. if y >= tb.visual_line_count() { x = CoordType::MAX; } tb.cursor_move_to_visual(Point { x, y }); // If we fell into the `if y >= tb.get_visual_line_count()` above, we wanted to // update the `preferred_column` but didn't know yet what it was. Now we know! if x == CoordType::MAX { tc.preferred_column = tb.cursor_visual_pos().x; } } kbmod::CTRL => { tc.scroll_offset.y += 1; make_cursor_visible = false; } kbmod::SHIFT => { // If the cursor was already on the last line, // move it to the end of the buffer. if tb.cursor_visual_pos().y >= tb.visual_line_count() - 1 { tc.preferred_column = CoordType::MAX; } tb.selection_update_visual(Point { x: tc.preferred_column, y: tb.cursor_visual_pos().y + 1, }); if tc.preferred_column == CoordType::MAX { tc.preferred_column = tb.cursor_visual_pos().x; } } kbmod::ALT => tb.move_selected_lines(MoveLineDirection::Down), kbmod::CTRL_ALT => { // TODO: Add cursor above } _ => return false, } } vk::INSERT => match modifiers { kbmod::SHIFT => tb.paste(self.clipboard_ref()), kbmod::CTRL => tb.copy(self.clipboard_mut()), _ => tb.set_overtype(!tb.is_overtype()), }, vk::DELETE => match modifiers { kbmod::SHIFT => tb.cut(self.clipboard_mut()), kbmod::CTRL => tb.delete(CursorMovement::Word, 1), _ => tb.delete(CursorMovement::Grapheme, 1), }, vk::A => match modifiers { kbmod::CTRL => tb.select_all(), _ => return false, }, vk::B => match modifiers { kbmod::ALT if cfg!(any(target_os = "macos", target_os = "ios")) => { // On macOS, terminals commonly emit the Emacs style // Alt+B (ESC b) sequence for Alt+Left. tb.cursor_move_delta(CursorMovement::Word, -1); } _ => return false, }, vk::F => match modifiers { kbmod::ALT if cfg!(any(target_os = "macos", target_os = "ios")) => { // On macOS, terminals commonly emit the Emacs style // Alt+F (ESC f) sequence for Alt+Right. tb.cursor_move_delta(CursorMovement::Word, 1); } _ => return false, }, vk::H => match modifiers { kbmod::CTRL => tb.delete(CursorMovement::Word, -1), _ => return false, }, vk::L => match modifiers { kbmod::CTRL => tb.select_line(), _ => return false, }, vk::X => match modifiers { kbmod::CTRL => tb.cut(self.clipboard_mut()), _ => return false, }, vk::C => match modifiers { kbmod::CTRL => tb.copy(self.clipboard_mut()), _ => return false, }, vk::V => match modifiers { kbmod::CTRL => tb.paste(self.clipboard_ref()), _ => return false, }, vk::Y => match modifiers { kbmod::CTRL => tb.redo(), _ => return false, }, vk::Z => match modifiers { kbmod::CTRL => tb.undo(), kbmod::CTRL_SHIFT => tb.redo(), kbmod::ALT => tb.set_word_wrap(!tb.is_word_wrap_enabled()), _ => return false, }, _ => return false, } change_preferred_column = !matches!(key, vk::PRIOR | vk::NEXT | vk::UP | vk::DOWN); } else { return false; } if single_line && !write.is_empty() { let (end, _) = simd::lines_fwd(write, 0, 0, 1); write = unicode::strip_newline(&write[..end]); } if !write.is_empty() { tb.write_canon(write); change_preferred_column = true; make_cursor_visible = true; } if change_preferred_column { tc.preferred_column = tb.cursor_visual_pos().x; } self.set_input_consumed(); make_cursor_visible } fn textarea_make_cursor_visible(&self, tc: &mut TextareaContent, node_prev: &Node) { let tb = tc.buffer.borrow(); let mut scroll_x = tc.scroll_offset.x; let mut scroll_y = tc.scroll_offset.y; let text_width = tb.text_width(); let cursor_x = tb.cursor_visual_pos().x; scroll_x = scroll_x.min(cursor_x - 10); scroll_x = scroll_x.max(cursor_x - text_width + 10); let viewport_height = node_prev.inner.height(); let cursor_y = tb.cursor_visual_pos().y; // Scroll up if the cursor is above the visible area. scroll_y = scroll_y.min(cursor_y); // Scroll down if the cursor is below the visible area. scroll_y = scroll_y.max(cursor_y - viewport_height + 1); tc.scroll_offset.x = scroll_x; tc.scroll_offset.y = scroll_y; } fn textarea_adjust_scroll_offset(&self, tc: &mut TextareaContent) { let tb = tc.buffer.borrow(); let mut scroll_x = tc.scroll_offset.x; let mut scroll_y = tc.scroll_offset.y; scroll_x = scroll_x.min(tc.scroll_offset_x_max.max(tb.cursor_visual_pos().x) - 10); scroll_x = scroll_x.max(0); scroll_y = scroll_y.clamp(0, tb.visual_line_count() - 1); if tb.is_word_wrap_enabled() { scroll_x = 0; } tc.scroll_offset.x = scroll_x; tc.scroll_offset.y = scroll_y; } /// Creates a scrollable area. pub fn scrollarea_begin(&mut self, classname: &'static str, intrinsic_size: Size) { self.block_begin(classname); let container_node = self.tree.last_node; { let mut container = self.tree.last_node.borrow_mut(); container.content = NodeContent::Scrollarea(ScrollareaContent { scroll_offset: Point::MIN, scroll_offset_y_drag_start: CoordType::MIN, thumb_height: 0, }); if intrinsic_size.width > 0 || intrinsic_size.height > 0 { container.intrinsic_size.width = intrinsic_size.width.max(0); container.intrinsic_size.height = intrinsic_size.height.max(0); container.intrinsic_size_set = true; } } self.block_begin("content"); self.inherit_focus(); // Ensure that attribute modifications apply to the outer container. self.tree.last_node = container_node; } /// Scrolls the current scrollable area to the given position. pub fn scrollarea_scroll_to(&mut self, pos: Point) { let mut container = self.tree.last_node.borrow_mut(); if let NodeContent::Scrollarea(sc) = &mut container.content { sc.scroll_offset = pos; } else { debug_assert!(false); } } /// Ends the current scrollarea block. pub fn scrollarea_end(&mut self) { self.block_end(); // content block self.block_end(); // outer container let mut container = self.tree.last_node.borrow_mut(); let container_id = container.id; let container_depth = container.depth; let Some(prev_container) = self.tui.prev_node_map.get(container_id) else { return; }; let prev_container = prev_container.borrow(); let NodeContent::Scrollarea(sc) = &mut container.content else { unreachable!(); }; if sc.scroll_offset == Point::MIN && let NodeContent::Scrollarea(sc_prev) = &prev_container.content { *sc = sc_prev.clone(); } if !self.input_consumed { let container_rect = prev_container.inner; if self.input_scroll_delta != Point::default() && container_rect.contains(self.tui.mouse_position) { sc.scroll_offset.x += self.input_scroll_delta.x; sc.scroll_offset.y += self.input_scroll_delta.y; self.set_input_consumed(); } else if self.tui.mouse_state != InputMouseState::None { match self.tui.mouse_state { InputMouseState::Left if self.tui.mouse_is_drag => { // We don't need to look up the previous track node, // since it has a fixed size based on the container size. let track_rect = Rect { left: container_rect.right, top: container_rect.top, right: container_rect.right + 1, bottom: container_rect.bottom, }; if track_rect.contains(self.tui.mouse_down_position) { if sc.scroll_offset_y_drag_start == CoordType::MIN { sc.scroll_offset_y_drag_start = sc.scroll_offset.y; } let content = prev_container.children.first.unwrap().borrow(); let content_rect = content.inner; let content_height = content_rect.height(); let track_height = track_rect.height(); let scrollable_height = content_height - track_height; if scrollable_height > 0 { let trackable = track_height - sc.thumb_height; let delta_y = self.tui.mouse_position.y - self.tui.mouse_down_position.y; sc.scroll_offset.y = sc.scroll_offset_y_drag_start + (delta_y as i64 * scrollable_height as i64 / trackable as i64) as CoordType; } self.set_input_consumed(); } } InputMouseState::Release => { sc.scroll_offset_y_drag_start = CoordType::MIN; } _ => {} } } else if self.tui.is_subtree_focused_alt(container_id, container_depth) && let Some(key) = self.input_keyboard { match key { vk::PRIOR => sc.scroll_offset.y -= prev_container.inner_clipped.height(), vk::NEXT => sc.scroll_offset.y += prev_container.inner_clipped.height(), vk::END => sc.scroll_offset.y = CoordType::MAX, vk::HOME => sc.scroll_offset.y = 0, _ => return, } self.set_input_consumed(); } } } /// Creates a list where exactly one item is selected. pub fn list_begin(&mut self, classname: &'static str) { self.block_begin(classname); self.attr_focusable(); let mut last_node = self.tree.last_node.borrow_mut(); let content = self .tui .prev_node_map .get(last_node.id) .and_then(|node| match &node.borrow().content { NodeContent::List(content) => { Some(ListContent { selected: content.selected, selected_node: None }) } _ => None, }) .unwrap_or(ListContent { selected: 0, selected_node: None }); last_node.attributes.focus_void = true; last_node.content = NodeContent::List(content); } /// Creates a list item with the given text. pub fn list_item(&mut self, select: bool, text: &str) -> ListSelection { self.styled_list_item_begin(); self.styled_label_add_text(text); self.styled_list_item_end(select) } /// Creates a list item consisting of a styled label. /// See [`Context::styled_label_begin`]. pub fn styled_list_item_begin(&mut self) { let list = self.tree.current_node; let idx = list.borrow().child_count; self.next_block_id_mixin(idx as u64); self.styled_label_begin("item"); self.styled_label_add_text(" "); self.attr_focusable(); } /// Ends the current styled list item. pub fn styled_list_item_end(&mut self, select: bool) -> ListSelection { self.styled_label_end(); let list = self.tree.current_node; let selected_before; let selected_now; let focused; { let mut list = list.borrow_mut(); let content = match &mut list.content { NodeContent::List(content) => content, _ => unreachable!(), }; let item = self.tree.last_node.borrow(); let item_id = item.id; selected_before = content.selected == item_id; focused = self.is_focused(); // Inherit the default selection & Click changes selection selected_now = selected_before || (select && content.selected == 0) || focused; // Note down the selected node for keyboard navigation. if selected_now { content.selected_node = Some(self.tree.last_node); if !selected_before { content.selected = item_id; self.needs_rerender(); } } } // Clicking an item activates it let clicked = !self.input_consumed && (self.input_mouse_click == 2 && self.was_mouse_down()); // Pressing Enter on a selected item activates it as well let entered = focused && selected_before && !self.input_consumed && matches!(self.input_keyboard, Some(vk::RETURN)); let activated = clicked || entered; if activated { self.set_input_consumed(); } if selected_before && activated { ListSelection::Activated } else if selected_now && !selected_before { ListSelection::Selected } else { ListSelection::Unchanged } } /// [`Context::steal_focus`], but for a list view. /// /// This exists, because didn't want to figure out how to get /// [`Context::styled_list_item_end`] to recognize a regular, /// programmatic focus steal. pub fn list_item_steal_focus(&mut self) { self.steal_focus(); match &mut self.tree.current_node.borrow_mut().content { NodeContent::List(content) => { content.selected = self.tree.last_node.borrow().id; content.selected_node = Some(self.tree.last_node); } _ => unreachable!(), } } /// Ends the current list block. pub fn list_end(&mut self) { self.block_end(); let contains_focus; let selected_now; let mut selected_next; { let list = self.tree.last_node.borrow(); contains_focus = self.tui.is_subtree_focused(&list); selected_now = match &list.content { NodeContent::List(content) => content.selected_node, _ => unreachable!(), }; selected_next = match selected_now.or(list.children.first) { Some(node) => node, None => return, }; } if contains_focus && !self.input_consumed && let Some(key) = self.input_keyboard && let Some(selected_now) = selected_now { let list = self.tree.last_node.borrow(); if let Some(prev_container) = self.tui.prev_node_map.get(list.id) { let mut consumed = true; match key { vk::PRIOR => { selected_next = selected_now; for _ in 0..prev_container.borrow().inner_clipped.height() - 1 { let node = selected_next.borrow(); selected_next = match node.siblings.prev { Some(node) => node, None => break, }; } } vk::NEXT => { selected_next = selected_now; for _ in 0..prev_container.borrow().inner_clipped.height() - 1 { let node = selected_next.borrow(); selected_next = match node.siblings.next { Some(node) => node, None => break, }; } } vk::END => { selected_next = list.children.last.unwrap_or(selected_next); } vk::HOME => { selected_next = list.children.first.unwrap_or(selected_next); } vk::UP => { selected_next = selected_now .borrow() .siblings .prev .or(list.children.last) .unwrap_or(selected_next); } vk::DOWN => { selected_next = selected_now .borrow() .siblings .next .or(list.children.first) .unwrap_or(selected_next); } _ => consumed = false, } if consumed { self.set_input_consumed(); } } } // Now that we know which item is selected we can mark it as such. if !opt_ptr_eq(selected_now, Some(selected_next)) && let NodeContent::List(content) = &mut self.tree.last_node.borrow_mut().content { content.selected_node = Some(selected_next); } // Now that we know which item is selected we can mark it as such. if let NodeContent::Text(content) = &mut selected_next.borrow_mut().content { unsafe { content.text.as_bytes_mut()[0] = b'>'; } } // If the list has focus, we also delegate focus to the selected item and colorize it. if contains_focus { { let mut node = selected_next.borrow_mut(); node.attributes.bg = self.indexed(IndexedColor::Green); node.attributes.fg = self.contrasted(self.indexed(IndexedColor::Green)); } self.steal_focus_for(selected_next); } } /// Creates a menubar, to be shown at the top of the screen. pub fn menubar_begin(&mut self) { self.table_begin("menubar"); self.attr_focus_well(); self.table_next_row(); } /// Appends a menu to the current menubar. /// /// Returns true if the menu is open. Continue appending items to it in that case. pub fn menubar_menu_begin(&mut self, text: &str, accelerator: char) -> bool { let accelerator = if cfg!(any(target_os = "macos", target_os = "ios")) { '\0' } else { accelerator }; let mixin = self.tree.current_node.borrow().child_count as u64; self.next_block_id_mixin(mixin); self.button_label( "menu_button", text, ButtonStyle::default().accelerator(accelerator).bracketed(false), ); self.attr_focusable(); self.attr_padding(Rect::two(0, 1)); let contains_focus = self.contains_focus(); let keyboard_focus = accelerator != '\0' && !contains_focus && self.consume_shortcut(kbmod::ALT | InputKey::new(accelerator as u32)); if contains_focus || keyboard_focus { self.attr_background_rgba(self.tui.floater_default_bg); self.attr_foreground_rgba(self.tui.floater_default_fg); if self.is_focused() { self.attr_background_rgba(self.indexed(IndexedColor::Green)); self.attr_foreground_rgba(self.contrasted(self.indexed(IndexedColor::Green))); } self.next_block_id_mixin(mixin); self.table_begin("flyout"); self.attr_float(FloatSpec { anchor: Anchor::Last, gravity_x: 0.0, gravity_y: 0.0, offset_x: 0.0, offset_y: 1.0, }); self.attr_border(); self.attr_focus_well(); if keyboard_focus { self.steal_focus(); } true } else { false } } /// Appends a button to the current menu. pub fn menubar_menu_button( &mut self, text: &str, accelerator: char, shortcut: InputKey, ) -> bool { self.menubar_menu_checkbox(text, accelerator, shortcut, false) } /// Appends a checkbox to the current menu. /// Returns true if the checkbox was activated. pub fn menubar_menu_checkbox( &mut self, text: &str, accelerator: char, shortcut: InputKey, checked: bool, ) -> bool { self.table_next_row(); self.attr_focusable(); // First menu item? Steal focus. if self.tree.current_node.borrow_mut().siblings.prev.is_none() { self.inherit_focus(); } if self.is_focused() { self.attr_background_rgba(self.indexed(IndexedColor::Green)); self.attr_foreground_rgba(self.contrasted(self.indexed(IndexedColor::Green))); } let clicked = self.button_activated() || self.consume_shortcut(InputKey::new(accelerator as u32)); self.button_label( "menu_checkbox", text, ButtonStyle::default().bracketed(false).checked(checked).accelerator(accelerator), ); self.menubar_shortcut(shortcut); if clicked { // TODO: This should reassign the previous focused path. self.needs_rerender(); Tui::clean_node_path(&mut self.tui.focused_node_path); } clicked } /// Ends the current menu. pub fn menubar_menu_end(&mut self) { self.table_end(); if !self.input_consumed && let Some(key) = self.input_keyboard && matches!(key, vk::ESCAPE | vk::UP | vk::DOWN) { if matches!(key, vk::UP | vk::DOWN) { // If the focus is on the menubar, and the user presses up/down, // focus the first/last item of the flyout respectively. let ln = self.tree.last_node.borrow(); if self.tui.is_node_focused(ln.parent.map_or(0, |n| n.borrow().id)) { let selected_next = if key == vk::UP { ln.children.last } else { ln.children.first }; if let Some(selected_next) = selected_next { self.steal_focus_for(selected_next); self.set_input_consumed(); } } } else if self.contains_focus() { // Otherwise, if the menu is the focused one and the // user presses Escape, pass focus back to the menubar. self.tui.pop_focusable_node(1); } } } /// Ends the current menubar. pub fn menubar_end(&mut self) { self.table_end(); } /// Renders a button label with an optional accelerator character /// May also renders a checkbox or square brackets for inline buttons fn button_label(&mut self, classname: &'static str, text: &str, style: ButtonStyle) { // Label prefix self.styled_label_begin(classname); if style.bracketed { self.styled_label_add_text("["); } if let Some(checked) = style.checked { self.styled_label_add_text(if checked { "🗹 " } else { " " }); } // Label text match style.accelerator { Some(accelerator) if accelerator.is_ascii_uppercase() => { // Complex case: // Locate the offset of the accelerator character in the label text let mut off = text.len(); for (i, c) in text.bytes().enumerate() { // Perfect match (uppercase character) --> stop if c as char == accelerator { off = i; break; } // Inexact match (lowercase character) --> use first hit if (c & !0x20) as char == accelerator && off == text.len() { off = i; } } if off < text.len() { // Add an underline to the accelerator. self.styled_label_add_text(&text[..off]); self.styled_label_set_attributes(Attributes::Underlined); self.styled_label_add_text(&text[off..off + 1]); self.styled_label_set_attributes(Attributes::None); self.styled_label_add_text(&text[off + 1..]); } else { // Add the accelerator in parentheses and underline it. let ch = accelerator as u8; self.styled_label_add_text(text); self.styled_label_add_text("("); self.styled_label_set_attributes(Attributes::Underlined); self.styled_label_add_text(unsafe { str_from_raw_parts(&ch, 1) }); self.styled_label_set_attributes(Attributes::None); self.styled_label_add_text(")"); } } _ => { // Simple case: // no accelerator character self.styled_label_add_text(text); } } // Label postfix if style.bracketed { self.styled_label_add_text("]"); } self.styled_label_end(); } fn menubar_shortcut(&mut self, shortcut: InputKey) { let shortcut_letter = shortcut.value() as u8 as char; if shortcut_letter.is_ascii_uppercase() { let mut shortcut_text = BString::empty(); if shortcut.modifiers_contains(kbmod::CTRL) { shortcut_text.push_str(self.arena(), self.tui.modifier_translations.ctrl); shortcut_text.push(self.arena(), '+'); } if shortcut.modifiers_contains(kbmod::ALT) { shortcut_text.push_str(self.arena(), self.tui.modifier_translations.alt); shortcut_text.push(self.arena(), '+'); } if shortcut.modifiers_contains(kbmod::SHIFT) { shortcut_text.push_str(self.arena(), self.tui.modifier_translations.shift); shortcut_text.push(self.arena(), '+'); } shortcut_text.push(self.arena(), shortcut_letter); self.label("shortcut", &shortcut_text); } else { self.block_begin("shortcut"); self.block_end(); } self.attr_padding(Rect { left: 2, top: 0, right: 2, bottom: 0 }); } } /// See [`Tree::visit_all`]. #[derive(Clone, Copy)] enum VisitControl { Continue, SkipChildren, Stop, } /// Stores the root of the "DOM" tree of the UI. struct Tree<'a> { tail: &'a NodeCell<'a>, root_first: &'a NodeCell<'a>, root_last: &'a NodeCell<'a>, last_node: &'a NodeCell<'a>, current_node: &'a NodeCell<'a>, count: usize, checksum: u64, } impl<'a> Tree<'a> { /// Creates a new tree inside the given arena. /// A single root node is added for the main contents. fn new(arena: &'a Arena) -> Self { let root = Self::alloc_node(arena); { let mut r = root.borrow_mut(); r.id = ROOT_ID; r.classname = "root"; r.attributes.focusable = true; r.attributes.focus_well = true; } Self { tail: root, root_first: root, root_last: root, last_node: root, current_node: root, count: 1, checksum: ROOT_ID, } } fn alloc_node(arena: &'a Arena) -> &'a NodeCell<'a> { arena.alloc_uninit().write(Default::default()) } /// Appends a child node to the current node. fn push_child(&mut self, node: &'a NodeCell<'a>) { let mut n = node.borrow_mut(); n.parent = Some(self.current_node); n.stack_parent = Some(self.current_node); { let mut p = self.current_node.borrow_mut(); n.siblings.prev = p.children.last; n.depth = p.depth + 1; if let Some(child_last) = p.children.last { let mut child_last = child_last.borrow_mut(); child_last.siblings.next = Some(node); } if p.children.first.is_none() { p.children.first = Some(node); } p.children.last = Some(node); p.child_count += 1; } n.prev = Some(self.tail); { let mut tail = self.tail.borrow_mut(); tail.next = Some(node); } self.tail = node; self.last_node = node; self.current_node = node; self.count += 1; // wymix is weak, but both checksum and node.id are proper random, so... it's not *that* bad. self.checksum = wymix(self.checksum, n.id); } /// Removes the current node from its parent and appends it as a new root. /// Used for [`Context::attr_float`]. fn move_node_to_root(&mut self, node: &'a NodeCell<'a>, anchor: Option<&'a NodeCell<'a>>) { let mut n = node.borrow_mut(); let Some(parent) = n.parent else { return; }; if let Some(sibling_prev) = n.siblings.prev { let mut sibling_prev = sibling_prev.borrow_mut(); sibling_prev.siblings.next = n.siblings.next; } if let Some(sibling_next) = n.siblings.next { let mut sibling_next = sibling_next.borrow_mut(); sibling_next.siblings.prev = n.siblings.prev; } { let mut p = parent.borrow_mut(); if opt_ptr_eq(p.children.first, Some(node)) { p.children.first = n.siblings.next; } if opt_ptr_eq(p.children.last, Some(node)) { p.children.last = n.siblings.prev; } p.child_count -= 1; } n.parent = anchor; n.depth = anchor.map_or(0, |n| n.borrow().depth + 1); n.siblings.prev = Some(self.root_last); n.siblings.next = None; self.root_last.borrow_mut().siblings.next = Some(node); self.root_last = node; } /// Completes the current node and moves focus to the parent. fn pop_stack(&mut self) { let current_node = self.current_node.borrow(); if let Some(stack_parent) = current_node.stack_parent { self.last_node = self.current_node; self.current_node = stack_parent; } } fn iterate_siblings( mut node: Option<&'a NodeCell<'a>>, ) -> impl Iterator> + use<'a> { iter::from_fn(move || { let n = node?; node = n.borrow().siblings.next; Some(n) }) } fn iterate_siblings_rev( mut node: Option<&'a NodeCell<'a>>, ) -> impl Iterator> + use<'a> { iter::from_fn(move || { let n = node?; node = n.borrow().siblings.prev; Some(n) }) } fn iterate_roots(&self) -> impl Iterator> + use<'a> { Self::iterate_siblings(Some(self.root_first)) } fn iterate_roots_rev(&self) -> impl Iterator> + use<'a> { Self::iterate_siblings_rev(Some(self.root_last)) } /// Visits all nodes under and including `root` in depth order. /// Starts with node `start`. /// /// WARNING: Breaks in hilarious ways if `start` is not within `root`. fn visit_all) -> VisitControl>( root: &'a NodeCell<'a>, start: &'a NodeCell<'a>, forward: bool, mut cb: T, ) { let root_depth = root.borrow().depth; let mut node = start; let children_idx = if forward { NodeChildren::FIRST } else { NodeChildren::LAST }; let siblings_idx = if forward { NodeSiblings::NEXT } else { NodeSiblings::PREV }; while { 'traverse: { match cb(node) { VisitControl::Continue => { // Depth first search: It has a child? Go there. if let Some(child) = node.borrow().children.get(children_idx) { node = child; break 'traverse; } } VisitControl::SkipChildren => {} VisitControl::Stop => return, } loop { // If we hit the root while going up, we restart the traversal at // `root` going down again until we hit `start` again. let n = node.borrow(); if n.depth <= root_depth { break 'traverse; } // Go to the parent's next sibling. --> Next subtree. if let Some(sibling) = n.siblings.get(siblings_idx) { node = sibling; break; } // Out of children? Go back to the parent. node = n.parent.unwrap(); } } // We're done once we wrapped around to the `start`. !ptr::eq(node, start) } {} } } /// A hashmap of node IDs to nodes. /// /// This map uses a simple open addressing scheme with linear probing. /// It's fast, simple, and sufficient for the small number of nodes we have. struct NodeMap<'a> { slots: &'a [Option<&'a NodeCell<'a>>], shift: usize, mask: u64, } impl Default for NodeMap<'static> { fn default() -> Self { Self { slots: &[None, None], shift: 63, mask: 0 } } } impl<'a> NodeMap<'a> { /// Creates a new node map for the given tree. fn new(arena: &'a Arena, tree: &Tree<'a>) -> Self { // Since we aren't expected to have millions of nodes, // we allocate 4x the number of slots for a 25% fill factor. let width = (4 * tree.count + 1).ilog2().max(1) as usize; let slots = 1 << width; let shift = 64 - width; let mask = (slots - 1) as u64; let slots = arena.alloc_slice(slots, None); let mut node = tree.root_first; loop { let n = node.borrow(); let mut slot = n.id >> shift; loop { if slots[slot as usize].is_none() { slots[slot as usize] = Some(node); break; } slot = (slot + 1) & mask; } node = match n.next { Some(node) => node, None => break, }; } Self { slots, shift, mask } } /// Gets a node by its ID. fn get(&self, id: u64) -> Option<&'a NodeCell<'a>> { let shift = self.shift; let mask = self.mask; let mut slot = id >> shift; loop { let node = self.slots[slot as usize]?; if node.borrow().id == id { return Some(node); } slot = (slot + 1) & mask; } } } struct FloatAttributes { // Specifies the origin of the container relative to the container size. [0, 1] gravity_x: f32, gravity_y: f32, // Specifies an offset from the origin in cells. offset_x: f32, offset_y: f32, } /// NOTE: Must not contain items that require drop(). #[derive(Default)] struct NodeAttributes { float: Option, position: Position, padding: Rect, bg: StraightRgba, fg: StraightRgba, reverse: bool, bordered: bool, focusable: bool, focus_well: bool, // Prevents focus from leaving via Tab focus_void: bool, // Prevents focus from entering via Tab } /// NOTE: Must not contain items that require drop(). struct ListContent<'a> { selected: u64, // Points to the Node that holds this ListContent instance, if any>. selected_node: Option<&'a NodeCell<'a>>, } /// NOTE: Must not contain items that require drop(). struct TableContent<'a> { columns: BVec<'a, CoordType>, cell_gap: Size, } /// NOTE: Must not contain items that require drop(). struct StyledTextChunk { offset: usize, fg: StraightRgba, attr: Attributes, } const INVALID_STYLED_TEXT_CHUNK: StyledTextChunk = StyledTextChunk { offset: usize::MAX, fg: StraightRgba::zero(), attr: Attributes::None }; /// NOTE: Must not contain items that require drop(). struct TextContent<'a> { text: BString<'a>, chunks: BVec<'a, StyledTextChunk>, overflow: Overflow, } /// NOTE: Must not contain items that require drop(). struct TextareaContent<'a> { buffer: &'a TextBufferCell, // Carries over between frames. scroll_offset: Point, scroll_offset_y_drag_start: CoordType, scroll_offset_x_max: CoordType, thumb_height: CoordType, preferred_column: CoordType, single_line: bool, has_focus: bool, } /// NOTE: Must not contain items that require drop(). #[derive(Clone)] struct ScrollareaContent { scroll_offset: Point, scroll_offset_y_drag_start: CoordType, thumb_height: CoordType, } /// NOTE: Must not contain items that require drop(). #[derive(Default)] enum NodeContent<'a> { #[default] None, List(ListContent<'a>), Modal(BString<'a>), // title Table(TableContent<'a>), Text(TextContent<'a>), Textarea(TextareaContent<'a>), Scrollarea(ScrollareaContent), } /// NOTE: Must not contain items that require drop(). #[derive(Default)] struct NodeSiblings<'a> { prev: Option<&'a NodeCell<'a>>, next: Option<&'a NodeCell<'a>>, } impl<'a> NodeSiblings<'a> { const PREV: usize = 0; const NEXT: usize = 1; fn get(&self, off: usize) -> Option<&'a NodeCell<'a>> { match off & 1 { 0 => self.prev, 1 => self.next, _ => unreachable!(), } } } /// NOTE: Must not contain items that require drop(). #[derive(Default)] struct NodeChildren<'a> { first: Option<&'a NodeCell<'a>>, last: Option<&'a NodeCell<'a>>, } impl<'a> NodeChildren<'a> { const FIRST: usize = 0; const LAST: usize = 1; fn get(&self, off: usize) -> Option<&'a NodeCell<'a>> { match off & 1 { 0 => self.first, 1 => self.last, _ => unreachable!(), } } } type NodeCell<'a> = SemiRefCell>; /// A node in the UI tree. /// /// NOTE: Must not contain items that require drop(). #[derive(Default)] struct Node<'a> { prev: Option<&'a NodeCell<'a>>, next: Option<&'a NodeCell<'a>>, stack_parent: Option<&'a NodeCell<'a>>, id: u64, classname: &'static str, parent: Option<&'a NodeCell<'a>>, depth: usize, siblings: NodeSiblings<'a>, children: NodeChildren<'a>, child_count: usize, attributes: NodeAttributes, content: NodeContent<'a>, intrinsic_size: Size, intrinsic_size_set: bool, outer: Rect, // in screen-space, calculated during layout inner: Rect, // in screen-space, calculated during layout outer_clipped: Rect, // in screen-space, calculated during layout, restricted to the viewport inner_clipped: Rect, // in screen-space, calculated during layout, restricted to the viewport } impl<'a> Node<'a> { /// Given an outer rectangle (including padding and borders) of this node, /// this returns the inner rectangle (excluding padding and borders). fn outer_to_inner(&self, mut outer: Rect) -> Rect { let l = self.attributes.bordered; let t = self.attributes.bordered; let r = self.attributes.bordered || matches!(self.content, NodeContent::Scrollarea(..)); let b = self.attributes.bordered; outer.left += self.attributes.padding.left + l as CoordType; outer.top += self.attributes.padding.top + t as CoordType; outer.right -= self.attributes.padding.right + r as CoordType; outer.bottom -= self.attributes.padding.bottom + b as CoordType; outer } /// Given an intrinsic size (excluding padding and borders) of this node, /// this returns the outer size (including padding and borders). fn intrinsic_to_outer(&self) -> Size { let l = self.attributes.bordered; let t = self.attributes.bordered; let r = self.attributes.bordered || matches!(self.content, NodeContent::Scrollarea(..)); let b = self.attributes.bordered; let mut size = self.intrinsic_size; size.width += self.attributes.padding.left + self.attributes.padding.right + l as CoordType + r as CoordType; size.height += self.attributes.padding.top + self.attributes.padding.bottom + t as CoordType + b as CoordType; size } /// Computes the intrinsic size of this node and its children. fn compute_intrinsic_size(&mut self, arena: &'a Arena) { match &mut self.content { NodeContent::Table(spec) => { // Calculate each row's height and the maximum width of each of its columns. for row in Tree::iterate_siblings(self.children.first) { let mut row = row.borrow_mut(); let mut row_height = 0; for (column, cell) in Tree::iterate_siblings(row.children.first).enumerate() { let mut cell = cell.borrow_mut(); cell.compute_intrinsic_size(arena); let size = cell.intrinsic_to_outer(); // If the spec.columns[] value is positive, it's an absolute width. // Otherwise, it's a fraction of the remaining space. // // TODO: The latter is computed incorrectly. // Example: If the items are "a","b","c" then the intrinsic widths are [1,1,1]. // If the column spec is [0,-3,-1], then this code assigns an intrinsic row // width of 3, but it should be 5 (1+1+3), because the spec says that the // last column (flexible 1/1) must be 3 times as wide as the 2nd one (1/3rd). // It's not a big deal yet, because such functionality isn't needed just yet. if column >= spec.columns.len() { spec.columns.push(arena, 0); } spec.columns[column] = spec.columns[column].max(size.width); row_height = row_height.max(size.height); } row.intrinsic_size.height = row_height; } // Assuming each column has the width of the widest cell in that column, // calculate the total width of the table. let total_gap_width = spec.cell_gap.width * spec.columns.len().saturating_sub(1) as CoordType; let total_inner_width = spec.columns.iter().sum::() + total_gap_width; let mut total_width = 0; let mut total_height = 0; // Assign the total width to each row. for row in Tree::iterate_siblings(self.children.first) { let mut row = row.borrow_mut(); row.intrinsic_size.width = total_inner_width; row.intrinsic_size_set = true; let size = row.intrinsic_to_outer(); total_width = total_width.max(size.width); total_height += size.height; } let total_gap_height = spec.cell_gap.height * self.child_count.saturating_sub(1) as CoordType; total_height += total_gap_height; // Assign the total width/height to the table. if !self.intrinsic_size_set { self.intrinsic_size.width = total_width; self.intrinsic_size.height = total_height; self.intrinsic_size_set = true; } } _ => { let mut max_width = 0; let mut total_height = 0; for child in Tree::iterate_siblings(self.children.first) { let mut child = child.borrow_mut(); child.compute_intrinsic_size(arena); let size = child.intrinsic_to_outer(); max_width = max_width.max(size.width); total_height += size.height; } if !self.intrinsic_size_set { self.intrinsic_size.width = max_width; self.intrinsic_size.height = total_height; self.intrinsic_size_set = true; } } } } /// Lays out the children of this node. /// The clip rect restricts "rendering" to a certain area (the viewport). fn layout_children(&mut self, clip: Rect) { if self.children.first.is_none() || self.inner.is_empty() { return; } match &mut self.content { NodeContent::Table(spec) => { let width = self.inner.right - self.inner.left; let mut x = self.inner.left; let mut y = self.inner.top; for row in Tree::iterate_siblings(self.children.first) { let mut row = row.borrow_mut(); let mut size = row.intrinsic_to_outer(); size.width = width; row.outer.left = x; row.outer.top = y; row.outer.right = x + size.width; row.outer.bottom = y + size.height; row.outer = row.outer.intersect(self.inner); row.inner = row.outer_to_inner(row.outer); row.outer_clipped = row.outer.intersect(clip); row.inner_clipped = row.inner.intersect(clip); let mut row_height = 0; for (column, cell) in Tree::iterate_siblings(row.children.first).enumerate() { let mut cell = cell.borrow_mut(); let mut size = cell.intrinsic_to_outer(); size.width = spec.columns[column]; cell.outer.left = x; cell.outer.top = y; cell.outer.right = x + size.width; cell.outer.bottom = y + size.height; cell.outer = cell.outer.intersect(self.inner); cell.inner = cell.outer_to_inner(cell.outer); cell.outer_clipped = cell.outer.intersect(clip); cell.inner_clipped = cell.inner.intersect(clip); x += size.width + spec.cell_gap.width; row_height = row_height.max(size.height); cell.layout_children(clip); } x = self.inner.left; y += row_height + spec.cell_gap.height; } } NodeContent::Scrollarea(sc) => { let mut content = self.children.first.unwrap().borrow_mut(); // content available viewport size (-1 for the track) let sx = self.inner.right - self.inner.left; let sy = self.inner.bottom - self.inner.top; // actual content size let cx = sx; let cy = content.intrinsic_size.height.max(sy); // scroll offset let ox = 0; let oy = sc.scroll_offset.y.clamp(0, cy - sy); sc.scroll_offset.x = ox; sc.scroll_offset.y = oy; content.outer.left = self.inner.left - ox; content.outer.top = self.inner.top - oy; content.outer.right = content.outer.left + cx; content.outer.bottom = content.outer.top + cy; content.inner = content.outer_to_inner(content.outer); content.outer_clipped = content.outer.intersect(self.inner_clipped); content.inner_clipped = content.inner.intersect(self.inner_clipped); let clip = content.inner_clipped; content.layout_children(clip); } _ => { let width = self.inner.right - self.inner.left; let x = self.inner.left; let mut y = self.inner.top; for child in Tree::iterate_siblings(self.children.first) { let mut child = child.borrow_mut(); let size = child.intrinsic_to_outer(); let remaining = (width - size.width).max(0); child.outer.left = x + match child.attributes.position { Position::Stretch | Position::Left => 0, Position::Center => remaining / 2, Position::Right => remaining, }; child.outer.right = child.outer.left + match child.attributes.position { Position::Stretch => width, _ => size.width, }; child.outer.top = y; child.outer.bottom = y + size.height; child.outer = child.outer.intersect(self.inner); child.inner = child.outer_to_inner(child.outer); child.outer_clipped = child.outer.intersect(clip); child.inner_clipped = child.inner.intersect(clip); y += size.height; } for child in Tree::iterate_siblings(self.children.first) { let mut child = child.borrow_mut(); child.layout_children(clip); } } } } } ================================================ FILE: crates/edit/src/unicode/measurement.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use stdext::cold_path; use stdext::unicode::Utf8Chars; use super::tables::*; use crate::document::ReadableDocument; use crate::helpers::{CoordType, Point}; // On one hand it's disgusting that I wrote this as a global variable, but on the // other hand, this isn't a public library API, and it makes the code a lot cleaner, // because we don't need to inject this once-per-process value everywhere. static mut AMBIGUOUS_WIDTH: usize = 1; /// Sets the width of "ambiguous" width characters as per "UAX #11: East Asian Width". /// /// Defaults to 1. pub fn setup_ambiguous_width(ambiguous_width: CoordType) { unsafe { AMBIGUOUS_WIDTH = ambiguous_width as usize }; } #[inline] fn ambiguous_width() -> usize { // SAFETY: This is a global variable that is set once per process. // It is never changed after that, so this is safe to call. unsafe { AMBIGUOUS_WIDTH } } /// Stores a position inside a [`ReadableDocument`]. /// /// The cursor tracks both the absolute byte-offset, /// as well as the position in terminal-related coordinates. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] pub struct Cursor { /// Offset in bytes within the buffer. pub offset: usize, /// Position in the buffer in lines (.y) and grapheme clusters (.x). /// /// Line wrapping has NO influence on this. pub logical_pos: Point, /// Position in the buffer in laid out rows (.y) and columns (.x). /// /// Line wrapping has an influence on this. pub visual_pos: Point, /// Horizontal position in visual columns. /// /// Line wrapping has NO influence on this and if word wrap is disabled, /// it's identical to `visual_pos.x`. This is useful for calculating tab widths. pub column: CoordType, /// When `measure_forward` hits the `word_wrap_column`, the question is: /// Was there a wrap opportunity on this line? Because if there wasn't, /// a hard-wrap is required; otherwise, the word that is being laid-out is /// moved to the next line. This boolean carries this state between calls. pub wrap_opp: bool, } /// Your entrypoint to navigating inside a [`ReadableDocument`]. #[derive(Clone)] pub struct MeasurementConfig<'doc> { cursor: Cursor, tab_size: CoordType, word_wrap_column: CoordType, buffer: &'doc dyn ReadableDocument, } impl<'doc> MeasurementConfig<'doc> { /// Creates a new [`MeasurementConfig`] for the given document. pub fn new(buffer: &'doc dyn ReadableDocument) -> Self { Self { cursor: Default::default(), tab_size: 8, word_wrap_column: 0, buffer } } /// Sets the initial cursor to the given position. /// /// WARNING: While the code doesn't panic if the cursor is invalid, /// the results will obviously be complete garbage. pub fn with_cursor(mut self, cursor: Cursor) -> Self { self.cursor = cursor; self } /// Sets the tab size. /// /// Defaults to 8, because that's what a tab in terminals evaluates to. pub fn with_tab_size(mut self, tab_size: CoordType) -> Self { self.tab_size = tab_size.max(1); self } /// You want word wrap? Set it here! /// /// Defaults to 0, which means no word wrap. pub fn with_word_wrap_column(mut self, word_wrap_column: CoordType) -> Self { self.word_wrap_column = word_wrap_column; self } /// Navigates **forward** to the given absolute offset. /// /// # Returns /// /// The cursor position after the navigation. pub fn goto_offset(&mut self, offset: usize) -> Cursor { self.measure_forward(offset, Point::MAX, Point::MAX) } /// Navigates **forward** to the given logical position. /// /// Logical positions are in lines and grapheme clusters. /// /// # Returns /// /// The cursor position after the navigation. pub fn goto_logical(&mut self, logical_target: Point) -> Cursor { self.measure_forward(usize::MAX, logical_target, Point::MAX) } /// Navigates **forward** to the given visual position. /// /// Visual positions are in laid out rows and columns. /// /// # Returns /// /// The cursor position after the navigation. pub fn goto_visual(&mut self, visual_target: Point) -> Cursor { self.measure_forward(usize::MAX, Point::MAX, visual_target) } /// Returns the current cursor position. pub fn cursor(&self) -> Cursor { self.cursor } // NOTE that going to a visual target can result in ambiguous results, // where going to an identical logical target will yield a different result. // // Imagine if you have a `word_wrap_column` of 6 and there's "Hello World" on the line: // `goto_logical` will return a `visual_pos` of {0,1}, while `goto_visual` returns {6,0}. // This is because from a logical POV, if the wrap location equals the wrap column, // the wrap exists on both lines and it'll default to wrapping. `goto_visual` however will always // try to return a Y position that matches the requested position, so that Home/End works properly. fn measure_forward( &mut self, offset_target: usize, logical_target: Point, visual_target: Point, ) -> Cursor { if self.cursor.offset >= offset_target || self.cursor.logical_pos >= logical_target || self.cursor.visual_pos >= visual_target { return self.cursor; } let mut offset = self.cursor.offset; let mut logical_pos_x = self.cursor.logical_pos.x; let mut logical_pos_y = self.cursor.logical_pos.y; let mut visual_pos_x = self.cursor.visual_pos.x; let mut visual_pos_y = self.cursor.visual_pos.y; let mut column = self.cursor.column; let mut logical_target_x = Self::calc_target_x(logical_target, logical_pos_y); let mut visual_target_x = Self::calc_target_x(visual_target, visual_pos_y); // wrap_opp = Wrap Opportunity // These store the position and column of the last wrap opportunity. If `word_wrap_column` is // zero (word wrap disabled), all grapheme clusters are a wrap opportunity, because none are. let mut wrap_opp = self.cursor.wrap_opp; let mut wrap_opp_offset = offset; let mut wrap_opp_logical_pos_x = logical_pos_x; let mut wrap_opp_visual_pos_x = visual_pos_x; let mut wrap_opp_column = column; let mut chunk_iter = Utf8Chars::new(b"", 0); let mut chunk_range = offset..offset; let mut props_next_cluster = ucd_start_of_text_properties(); loop { // Have we reached the target already? Stop. if offset >= offset_target || logical_pos_x >= logical_target_x || visual_pos_x >= visual_target_x { break; } let props_current_cluster = props_next_cluster; let mut props_last_char; let mut offset_next_cluster; let mut state = 0; let mut width = 0; // Since we want to measure the width of the current cluster, // by necessity we need to seek to the next cluster. // We'll then reuse the offset and properties of the next cluster in // the next iteration of the this (outer) loop (`props_next_cluster`). loop { if !chunk_iter.has_next() { cold_path(); chunk_iter = Utf8Chars::new(self.buffer.read_forward(chunk_range.end), 0); chunk_range = chunk_range.end..chunk_range.end + chunk_iter.len(); } // Since this loop seeks ahead to the next cluster, and since `chunk_iter` // records the offset of the next character after the returned one, we need // to save the offset of the previous `chunk_iter` before calling `next()`. // Similar applies to the width. props_last_char = props_next_cluster; offset_next_cluster = chunk_range.start + chunk_iter.offset(); width += ucd_grapheme_cluster_character_width(props_next_cluster, ambiguous_width()) as CoordType; // The `Document::read_forward` interface promises us that it will not split // grapheme clusters across chunks. Therefore, we can safely break here. let ch = match chunk_iter.next() { Some(ch) => ch, None => break, }; // Get the properties of the next cluster. props_next_cluster = ucd_grapheme_cluster_lookup(ch); state = ucd_grapheme_cluster_joins(state, props_last_char, props_next_cluster); // Stop if the next character does not join. if ucd_grapheme_cluster_joins_done(state) { break; } } if offset_next_cluster == offset { // No advance and the iterator is empty? End of text reached. if chunk_iter.is_empty() { break; } // Ignore the first iteration when processing the start-of-text. continue; } // The max. width of a terminal cell is 2. width = width.min(2); // Tabs require special handling because they can have a variable width. if props_last_char == ucd_tab_properties() { // SAFETY: `self.tab_size` is clamped to >= 1 in `with_tab_size`. // This assert ensures that Rust doesn't insert panicking null checks. unsafe { std::hint::assert_unchecked(self.tab_size >= 1) }; width = self.tab_size - (column % self.tab_size); } // Hard wrap: Both the logical and visual position advance by one line. if props_last_char == ucd_linefeed_properties() { cold_path(); wrap_opp = false; // Don't cross the newline if the target is on this line but we haven't reached it. // E.g. if the callers asks for column 100 on a 10 column line, // we'll return with the cursor set to column 10. if logical_pos_y >= logical_target.y || visual_pos_y >= visual_target.y { break; } offset = offset_next_cluster; logical_pos_x = 0; logical_pos_y += 1; visual_pos_x = 0; visual_pos_y += 1; column = 0; logical_target_x = Self::calc_target_x(logical_target, logical_pos_y); visual_target_x = Self::calc_target_x(visual_target, visual_pos_y); continue; } // Avoid advancing past the visual target, because `width` can be greater than 1. if visual_pos_x + width > visual_target_x { break; } // Since this code above may need to revert to a previous `wrap_opp_*`, // it must be done before advancing / checking for `ucd_line_break_joins`. if self.word_wrap_column > 0 && visual_pos_x + width > self.word_wrap_column { if !wrap_opp { // Otherwise, the lack of a wrap opportunity means that a single word // is wider than the word wrap column. We need to force-break the word. // This is similar to the above, but "bar" gets written at column 0. wrap_opp_offset = offset; wrap_opp_logical_pos_x = logical_pos_x; wrap_opp_visual_pos_x = visual_pos_x; wrap_opp_column = column; visual_pos_x = 0; } else { // If we had a wrap opportunity on this line, we can move all // characters since then to the next line without stopping this loop: // +---------+ +---------+ +---------+ // | foo| -> | | -> | | // | | |foo | |foobar | // +---------+ +---------+ +---------+ // We don't actually move "foo", but rather just change where "bar" goes. // Since this function doesn't copy text, the end result is the same. visual_pos_x -= wrap_opp_visual_pos_x; } wrap_opp = false; visual_pos_y += 1; visual_target_x = Self::calc_target_x(visual_target, visual_pos_y); if visual_pos_x == visual_target_x { break; } // Imagine the word is "hello" and on the "o" we notice it wraps. // If the target however was the "e", then we must revert back to "h" and search for it. if visual_pos_x > visual_target_x { cold_path(); offset = wrap_opp_offset; logical_pos_x = wrap_opp_logical_pos_x; visual_pos_x = 0; column = wrap_opp_column; chunk_iter.seek(chunk_iter.len()); chunk_range = offset..offset; props_next_cluster = ucd_start_of_text_properties(); continue; } } offset = offset_next_cluster; logical_pos_x += 1; visual_pos_x += width; column += width; if self.word_wrap_column > 0 && !ucd_line_break_joins(props_current_cluster, props_next_cluster) { wrap_opp = true; wrap_opp_offset = offset; wrap_opp_logical_pos_x = logical_pos_x; wrap_opp_visual_pos_x = visual_pos_x; wrap_opp_column = column; } } // If we're here, we hit our target. Now the only question is: // Is the word we're currently on so wide that it will be wrapped further down the document? if self.word_wrap_column > 0 { if !wrap_opp { // If the current laid-out line had no wrap opportunities, it means we had an input // such as "fooooooooooooooooooooo" at a `word_wrap_column` of e.g. 10. The word // didn't fit and the lack of a `wrap_opp` indicates we must force a hard wrap. // Thankfully, if we reach this point, that was already done by the code above. } else if wrap_opp_logical_pos_x != logical_pos_x && visual_pos_y <= visual_target.y { // Imagine the string "foo bar" with a word wrap column of 6. If I ask for the cursor at // `logical_pos={5,0}`, then the code above exited while reaching the target. // At this point, this function doesn't know yet that after the "b" there's "ar" // which causes a word wrap, and causes the final visual position to be {1,1}. // This code thus seeks ahead and checks if the current word will wrap or not. // Of course we only need to do this if the cursor isn't on a wrap opportunity already. // The loop below should not modify the target we already found. let mut visual_pos_x_lookahead = visual_pos_x; loop { let props_current_cluster = props_next_cluster; let mut props_last_char; let mut offset_next_cluster; let mut state = 0; let mut width = 0; // Since we want to measure the width of the current cluster, // by necessity we need to seek to the next cluster. // We'll then reuse the offset and properties of the next cluster in // the next iteration of the this (outer) loop (`props_next_cluster`). loop { if !chunk_iter.has_next() { cold_path(); chunk_iter = Utf8Chars::new(self.buffer.read_forward(chunk_range.end), 0); chunk_range = chunk_range.end..chunk_range.end + chunk_iter.len(); } // Since this loop seeks ahead to the next cluster, and since `chunk_iter` // records the offset of the next character after the returned one, we need // to save the offset of the previous `chunk_iter` before calling `next()`. // Similar applies to the width. props_last_char = props_next_cluster; offset_next_cluster = chunk_range.start + chunk_iter.offset(); width += ucd_grapheme_cluster_character_width( props_next_cluster, ambiguous_width(), ) as CoordType; // The `Document::read_forward` interface promises us that it will not split // grapheme clusters across chunks. Therefore, we can safely break here. let ch = match chunk_iter.next() { Some(ch) => ch, None => break, }; // Get the properties of the next cluster. props_next_cluster = ucd_grapheme_cluster_lookup(ch); state = ucd_grapheme_cluster_joins(state, props_last_char, props_next_cluster); // Stop if the next character does not join. if ucd_grapheme_cluster_joins_done(state) { break; } } if offset_next_cluster == offset { // No advance and the iterator is empty? End of text reached. if chunk_iter.is_empty() { break; } // Ignore the first iteration when processing the start-of-text. continue; } // The max. width of a terminal cell is 2. width = width.min(2); // Tabs require special handling because they can have a variable width. if props_last_char == ucd_tab_properties() { // SAFETY: `self.tab_size` is clamped to >= 1 in `with_tab_size`. // This assert ensures that Rust doesn't insert panicking null checks. unsafe { std::hint::assert_unchecked(self.tab_size >= 1) }; width = self.tab_size - (column % self.tab_size); } // Hard wrap: Both the logical and visual position advance by one line. if props_last_char == ucd_linefeed_properties() { break; } visual_pos_x_lookahead += width; if visual_pos_x_lookahead > self.word_wrap_column { visual_pos_x -= wrap_opp_visual_pos_x; visual_pos_y += 1; break; } else if !ucd_line_break_joins(props_current_cluster, props_next_cluster) { break; } } } if visual_pos_y > visual_target.y { // Imagine the string "foo bar" with a word wrap column of 6. If I ask for the cursor at // `visual_pos={100,0}`, the code above exited early after wrapping without reaching the target. // Since I asked for the last character on the first line, we must wrap back up the last wrap offset = wrap_opp_offset; logical_pos_x = wrap_opp_logical_pos_x; visual_pos_x = wrap_opp_visual_pos_x; visual_pos_y = visual_target.y; column = wrap_opp_column; wrap_opp = true; } } self.cursor.offset = offset; self.cursor.logical_pos = Point { x: logical_pos_x, y: logical_pos_y }; self.cursor.visual_pos = Point { x: visual_pos_x, y: visual_pos_y }; self.cursor.column = column; self.cursor.wrap_opp = wrap_opp; self.cursor } #[inline] fn calc_target_x(target: Point, pos_y: CoordType) -> CoordType { match pos_y.cmp(&target.y) { std::cmp::Ordering::Less => CoordType::MAX, std::cmp::Ordering::Equal => target.x, std::cmp::Ordering::Greater => 0, } } } /// Returns an offset past a newline. /// /// If `offset` is right in front of a newline, /// this will return the offset past said newline. pub fn skip_newline(text: &[u8], mut offset: usize) -> usize { if offset >= text.len() { return offset; } if text[offset] == b'\r' { offset += 1; } if offset >= text.len() { return offset; } if text[offset] == b'\n' { offset += 1; } offset } /// Strips a trailing newline from the given text. pub fn strip_newline(mut text: &[u8]) -> &[u8] { // Rust generates surprisingly tight assembly for this. if text.last() == Some(&b'\n') { text = &text[..text.len() - 1]; } if text.last() == Some(&b'\r') { text = &text[..text.len() - 1]; } text } #[cfg(test)] mod test { use super::*; struct ChunkedDoc<'a>(&'a [&'a [u8]]); impl ReadableDocument for ChunkedDoc<'_> { fn read_forward(&self, mut off: usize) -> &[u8] { for chunk in self.0 { if off < chunk.len() { return &chunk[off..]; } off -= chunk.len(); } &[] } fn read_backward(&self, mut off: usize) -> &[u8] { for chunk in self.0.iter().rev() { if off < chunk.len() { return &chunk[..chunk.len() - off]; } off -= chunk.len(); } &[] } } #[test] fn test_measure_forward_newline_start() { let cursor = MeasurementConfig::new(&"foo\nbar".as_bytes()).goto_visual(Point { x: 0, y: 1 }); assert_eq!( cursor, Cursor { offset: 4, logical_pos: Point { x: 0, y: 1 }, visual_pos: Point { x: 0, y: 1 }, column: 0, wrap_opp: false, } ); } #[test] fn test_measure_forward_clipped_wide_char() { let cursor = MeasurementConfig::new(&"a😶‍🌫️b".as_bytes()).goto_visual(Point { x: 2, y: 0 }); assert_eq!( cursor, Cursor { offset: 1, logical_pos: Point { x: 1, y: 0 }, visual_pos: Point { x: 1, y: 0 }, column: 1, wrap_opp: false, } ); } #[test] fn test_measure_forward_word_wrap() { // |foo␣ | // |bar␣ | // |baz | let text = "foo bar \nbaz".as_bytes(); // Does hitting a logical target wrap the visual position along with the word? let mut cfg = MeasurementConfig::new(&text).with_word_wrap_column(6); let cursor = cfg.goto_logical(Point { x: 5, y: 0 }); assert_eq!( cursor, Cursor { offset: 5, logical_pos: Point { x: 5, y: 0 }, visual_pos: Point { x: 1, y: 1 }, column: 5, wrap_opp: true, } ); // Does hitting the visual target within a word reset the hit back to the end of the visual line? let mut cfg = MeasurementConfig::new(&text).with_word_wrap_column(6); let cursor = cfg.goto_visual(Point { x: CoordType::MAX, y: 0 }); assert_eq!( cursor, Cursor { offset: 4, logical_pos: Point { x: 4, y: 0 }, visual_pos: Point { x: 4, y: 0 }, column: 4, wrap_opp: true, } ); // Does hitting the same target but with a non-zero starting position result in the same outcome? let mut cfg = MeasurementConfig::new(&text).with_word_wrap_column(6).with_cursor(Cursor { offset: 1, logical_pos: Point { x: 1, y: 0 }, visual_pos: Point { x: 1, y: 0 }, column: 1, wrap_opp: false, }); let cursor = cfg.goto_visual(Point { x: 5, y: 0 }); assert_eq!( cursor, Cursor { offset: 4, logical_pos: Point { x: 4, y: 0 }, visual_pos: Point { x: 4, y: 0 }, column: 4, wrap_opp: true, } ); let cursor = cfg.goto_visual(Point { x: 0, y: 1 }); assert_eq!( cursor, Cursor { offset: 4, logical_pos: Point { x: 4, y: 0 }, visual_pos: Point { x: 0, y: 1 }, column: 4, wrap_opp: false, } ); let cursor = cfg.goto_visual(Point { x: 5, y: 1 }); assert_eq!( cursor, Cursor { offset: 8, logical_pos: Point { x: 8, y: 0 }, visual_pos: Point { x: 4, y: 1 }, column: 8, wrap_opp: false, } ); let cursor = cfg.goto_visual(Point { x: 0, y: 2 }); assert_eq!( cursor, Cursor { offset: 9, logical_pos: Point { x: 0, y: 1 }, visual_pos: Point { x: 0, y: 2 }, column: 0, wrap_opp: false, } ); let cursor = cfg.goto_visual(Point { x: 5, y: 2 }); assert_eq!( cursor, Cursor { offset: 12, logical_pos: Point { x: 3, y: 1 }, visual_pos: Point { x: 3, y: 2 }, column: 3, wrap_opp: false, } ); } #[test] fn test_measure_forward_tabs() { let text = "a\tb\tc".as_bytes(); let cursor = MeasurementConfig::new(&text).with_tab_size(4).goto_visual(Point { x: 4, y: 0 }); assert_eq!( cursor, Cursor { offset: 2, logical_pos: Point { x: 2, y: 0 }, visual_pos: Point { x: 4, y: 0 }, column: 4, wrap_opp: false, } ); } #[test] fn test_measure_forward_chunk_boundaries() { let chunks = [ "Hello".as_bytes(), "\u{1F469}\u{1F3FB}".as_bytes(), // 8 bytes, 2 columns "World".as_bytes(), ]; let doc = ChunkedDoc(&chunks); let cursor = MeasurementConfig::new(&doc).goto_visual(Point { x: 5 + 2 + 3, y: 0 }); assert_eq!(cursor.offset, 5 + 8 + 3); assert_eq!(cursor.logical_pos, Point { x: 5 + 1 + 3, y: 0 }); } #[test] fn test_exact_wrap() { // |foo_ | // |bar. | // |abc | let chunks = ["foo ".as_bytes(), "bar".as_bytes(), ".\n".as_bytes(), "abc".as_bytes()]; let doc = ChunkedDoc(&chunks); let mut cfg = MeasurementConfig::new(&doc).with_word_wrap_column(7); let max = CoordType::MAX; let end0 = cfg.goto_visual(Point { x: 7, y: 0 }); assert_eq!( end0, Cursor { offset: 4, logical_pos: Point { x: 4, y: 0 }, visual_pos: Point { x: 4, y: 0 }, column: 4, wrap_opp: true, } ); let beg1 = cfg.goto_visual(Point { x: 0, y: 1 }); assert_eq!( beg1, Cursor { offset: 4, logical_pos: Point { x: 4, y: 0 }, visual_pos: Point { x: 0, y: 1 }, column: 4, wrap_opp: false, } ); let end1 = cfg.goto_visual(Point { x: max, y: 1 }); assert_eq!( end1, Cursor { offset: 8, logical_pos: Point { x: 8, y: 0 }, visual_pos: Point { x: 4, y: 1 }, column: 8, wrap_opp: false, } ); let beg2 = cfg.goto_visual(Point { x: 0, y: 2 }); assert_eq!( beg2, Cursor { offset: 9, logical_pos: Point { x: 0, y: 1 }, visual_pos: Point { x: 0, y: 2 }, column: 0, wrap_opp: false, } ); let end2 = cfg.goto_visual(Point { x: max, y: 2 }); assert_eq!( end2, Cursor { offset: 12, logical_pos: Point { x: 3, y: 1 }, visual_pos: Point { x: 3, y: 2 }, column: 3, wrap_opp: false, } ); } #[test] fn test_force_wrap() { // |//_ | // |aaaaaaaa| // |aaaa | let bytes = "// aaaaaaaaaaaa".as_bytes(); let mut cfg = MeasurementConfig::new(&bytes).with_word_wrap_column(8); let max = CoordType::MAX; // At the end of "// " there should be a wrap. let end0 = cfg.goto_visual(Point { x: max, y: 0 }); assert_eq!( end0, Cursor { offset: 3, logical_pos: Point { x: 3, y: 0 }, visual_pos: Point { x: 3, y: 0 }, column: 3, wrap_opp: true, } ); // Test if the ambiguous visual position at the wrap location doesn't change the offset. let beg0 = cfg.goto_visual(Point { x: 0, y: 1 }); assert_eq!( beg0, Cursor { offset: 3, logical_pos: Point { x: 3, y: 0 }, visual_pos: Point { x: 0, y: 1 }, column: 3, wrap_opp: false, } ); // Test if navigating inside the wrapped line doesn't cause further wrapping. // // This step of the test is important, as it ensures that the following force-wrap works, // even if 1 of the 8 "a"s was already processed. let beg0_off1 = cfg.goto_logical(Point { x: 4, y: 0 }); assert_eq!( beg0_off1, Cursor { offset: 4, logical_pos: Point { x: 4, y: 0 }, visual_pos: Point { x: 1, y: 1 }, column: 4, wrap_opp: false, } ); // Test if the force-wrap applies at the end of the first 8 "a"s. let end1 = cfg.goto_visual(Point { x: max, y: 1 }); assert_eq!( end1, Cursor { offset: 11, logical_pos: Point { x: 11, y: 0 }, visual_pos: Point { x: 8, y: 1 }, column: 11, wrap_opp: true, } ); // Test if the remaining 4 "a"s are properly laid-out. let end2 = cfg.goto_visual(Point { x: max, y: 2 }); assert_eq!( end2, Cursor { offset: 15, logical_pos: Point { x: 15, y: 0 }, visual_pos: Point { x: 4, y: 2 }, column: 15, wrap_opp: false, } ); } #[test] fn test_force_wrap_wide() { // These Yijing Hexagram Symbols form no word wrap opportunities. let text = "䷀䷁䷂䷃䷄䷅䷆䷇䷈䷉"; let expected = ["䷀䷁", "䷂䷃", "䷄䷅", "䷆䷇", "䷈䷉"]; let bytes = text.as_bytes(); let mut cfg = MeasurementConfig::new(&bytes).with_word_wrap_column(5); for (y, &expected) in expected.iter().enumerate() { let y = y as CoordType; // In order for `goto_visual()` to hit column 0 after a word wrap, // it MUST be able to go back by 1 grapheme, which is what this tests. let beg = cfg.goto_visual(Point { x: 0, y }); let end = cfg.goto_visual(Point { x: 5, y }); let actual = &text[beg.offset..end.offset]; assert_eq!(actual, expected); } } // Similar to the `test_force_wrap` test, but here we vertically descend // down the document without ever touching the first or last column. // I found that this finds curious bugs at times. #[test] fn test_force_wrap_column() { // |//_ | // |aaaaaaaa| // |aaaa | let bytes = "// aaaaaaaaaaaa".as_bytes(); let mut cfg = MeasurementConfig::new(&bytes).with_word_wrap_column(8); // At the end of "// " there should be a wrap. let end0 = cfg.goto_visual(Point { x: CoordType::MAX, y: 0 }); assert_eq!( end0, Cursor { offset: 3, logical_pos: Point { x: 3, y: 0 }, visual_pos: Point { x: 3, y: 0 }, column: 3, wrap_opp: true, } ); let mid1 = cfg.goto_visual(Point { x: end0.visual_pos.x, y: 1 }); assert_eq!( mid1, Cursor { offset: 6, logical_pos: Point { x: 6, y: 0 }, visual_pos: Point { x: 3, y: 1 }, column: 6, wrap_opp: false, } ); let mid2 = cfg.goto_visual(Point { x: end0.visual_pos.x, y: 2 }); assert_eq!( mid2, Cursor { offset: 14, logical_pos: Point { x: 14, y: 0 }, visual_pos: Point { x: 3, y: 2 }, column: 14, wrap_opp: false, } ); } #[test] fn test_any_wrap() { // |//_-----| // |------- | let bytes = "// ------------".as_bytes(); let mut cfg = MeasurementConfig::new(&bytes).with_word_wrap_column(8); let max = CoordType::MAX; let end0 = cfg.goto_visual(Point { x: max, y: 0 }); assert_eq!( end0, Cursor { offset: 8, logical_pos: Point { x: 8, y: 0 }, visual_pos: Point { x: 8, y: 0 }, column: 8, wrap_opp: true, } ); let end1 = cfg.goto_visual(Point { x: max, y: 1 }); assert_eq!( end1, Cursor { offset: 15, logical_pos: Point { x: 15, y: 0 }, visual_pos: Point { x: 7, y: 1 }, column: 15, wrap_opp: true, } ); } #[test] fn test_any_wrap_wide() { // These Japanese characters form word wrap opportunity between each character. let text = "零一二三四五六七八九"; let expected = ["零一", "二三", "四五", "六七", "八九"]; let bytes = text.as_bytes(); let mut cfg = MeasurementConfig::new(&bytes).with_word_wrap_column(5); for (y, &expected) in expected.iter().enumerate() { let y = y as CoordType; // In order for `goto_visual()` to hit column 0 after a word wrap, // it MUST be able to go back by 1 grapheme, which is what this tests. let beg = cfg.goto_visual(Point { x: 0, y }); let end = cfg.goto_visual(Point { x: 5, y }); let actual = &text[beg.offset..end.offset]; assert_eq!(actual, expected); } } #[test] fn test_wrap_tab() { // |foo_ | <- 1 space // |____b | <- 1 tab, 1 space let text = "foo \t b"; let bytes = text.as_bytes(); let mut cfg = MeasurementConfig::new(&bytes).with_word_wrap_column(8).with_tab_size(4); let max = CoordType::MAX; let end0 = cfg.goto_visual(Point { x: max, y: 0 }); assert_eq!( end0, Cursor { offset: 4, logical_pos: Point { x: 4, y: 0 }, visual_pos: Point { x: 4, y: 0 }, column: 4, wrap_opp: true, }, ); let beg1 = cfg.goto_visual(Point { x: 0, y: 1 }); assert_eq!( beg1, Cursor { offset: 4, logical_pos: Point { x: 4, y: 0 }, visual_pos: Point { x: 0, y: 1 }, column: 4, wrap_opp: false, }, ); let end1 = cfg.goto_visual(Point { x: max, y: 1 }); assert_eq!( end1, Cursor { offset: 7, logical_pos: Point { x: 7, y: 0 }, visual_pos: Point { x: 6, y: 1 }, column: 10, wrap_opp: true, }, ); } #[test] fn test_crlf() { let text = "a\r\nbcd\r\ne".as_bytes(); let cursor = MeasurementConfig::new(&text).goto_visual(Point { x: CoordType::MAX, y: 1 }); assert_eq!( cursor, Cursor { offset: 6, logical_pos: Point { x: 3, y: 1 }, visual_pos: Point { x: 3, y: 1 }, column: 3, wrap_opp: false, } ); } #[test] fn test_wrapped_cursor_can_seek_backward() { let bytes = "hello world".as_bytes(); let mut cfg = MeasurementConfig::new(&bytes).with_word_wrap_column(10); // When the word wrap at column 10 hits, the cursor will be at the end of the word "world" (between l and d). // This tests if the algorithm is capable of going back to the start of the word and find the actual target. let cursor = cfg.goto_visual(Point { x: 2, y: 1 }); assert_eq!( cursor, Cursor { offset: 8, logical_pos: Point { x: 8, y: 0 }, visual_pos: Point { x: 2, y: 1 }, column: 8, wrap_opp: false, } ); } #[test] fn test_strip_newline() { assert_eq!(strip_newline(b"hello\n"), b"hello"); assert_eq!(strip_newline(b"hello\r\n"), b"hello"); assert_eq!(strip_newline(b"hello"), b"hello"); } } ================================================ FILE: crates/edit/src/unicode/mod.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! Everything related to Unicode lives here. mod measurement; mod tables; pub use measurement::*; ================================================ FILE: crates/edit/src/unicode/tables.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. // BEGIN: Generated by grapheme-table-gen on 2025-06-03T13:50:48Z, from Unicode 16.0.0, with --lang=rust --extended --line-breaks, 17688 bytes #[rustfmt::skip] const STAGE0: [u16; 544] = [ 0x0000, 0x0040, 0x007f, 0x00bf, 0x00ff, 0x013f, 0x017f, 0x0194, 0x0194, 0x01a6, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x01e6, 0x0226, 0x024a, 0x024b, 0x024c, 0x0246, 0x0255, 0x0295, 0x02d5, 0x02d5, 0x02d5, 0x030d, 0x034d, 0x038d, 0x03cd, 0x040d, 0x044d, 0x0478, 0x04b8, 0x04db, 0x04fc, 0x0295, 0x0295, 0x0295, 0x0534, 0x0574, 0x0194, 0x0194, 0x05b4, 0x05f4, 0x0295, 0x0295, 0x0295, 0x061d, 0x065d, 0x067d, 0x0295, 0x06a3, 0x06e3, 0x0723, 0x0763, 0x07a3, 0x07e3, 0x0823, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0863, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0194, 0x0863, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x08a3, 0x08b3, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x0295, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x08f3, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x02d5, 0x08f3, ]; #[rustfmt::skip] const STAGE1: [u16; 2355] = [ 0x0000, 0x0008, 0x0010, 0x0018, 0x0020, 0x0028, 0x0030, 0x0038, 0x0040, 0x0047, 0x004f, 0x0056, 0x0059, 0x0059, 0x005e, 0x0059, 0x0059, 0x0059, 0x0066, 0x006a, 0x0059, 0x0059, 0x0071, 0x0059, 0x0079, 0x0079, 0x007e, 0x0086, 0x008e, 0x0096, 0x009e, 0x0059, 0x00a6, 0x00aa, 0x00ae, 0x0059, 0x00b6, 0x0059, 0x0059, 0x0059, 0x0059, 0x00ba, 0x00bf, 0x0059, 0x00c6, 0x00cb, 0x00d3, 0x00d9, 0x00e1, 0x0059, 0x00e9, 0x00f1, 0x0059, 0x0059, 0x00f6, 0x00fe, 0x0105, 0x010a, 0x0110, 0x0059, 0x0059, 0x0117, 0x011f, 0x0125, 0x012d, 0x0134, 0x013c, 0x0144, 0x0149, 0x0059, 0x0151, 0x0159, 0x0161, 0x0167, 0x016f, 0x0177, 0x017f, 0x0185, 0x018d, 0x0195, 0x019d, 0x01a3, 0x01ab, 0x01b3, 0x01bb, 0x01c1, 0x01c9, 0x01d1, 0x01d9, 0x01df, 0x01e7, 0x01ef, 0x01f7, 0x01ff, 0x0207, 0x020e, 0x0216, 0x021c, 0x0224, 0x022c, 0x0234, 0x023a, 0x0242, 0x024a, 0x0252, 0x0258, 0x0260, 0x0268, 0x0270, 0x0277, 0x027f, 0x0287, 0x028d, 0x0291, 0x0299, 0x028d, 0x028d, 0x02a0, 0x02a8, 0x028d, 0x02b0, 0x02b8, 0x00bc, 0x02c0, 0x02c8, 0x02cf, 0x02d7, 0x028d, 0x02de, 0x02e6, 0x02ee, 0x02f6, 0x0059, 0x02fe, 0x0059, 0x0306, 0x0306, 0x0306, 0x030e, 0x030e, 0x0314, 0x0316, 0x0316, 0x0059, 0x0059, 0x031e, 0x0059, 0x0326, 0x032a, 0x0332, 0x0059, 0x0338, 0x0059, 0x033e, 0x0346, 0x034e, 0x0059, 0x0059, 0x0356, 0x035e, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0366, 0x0059, 0x0059, 0x036e, 0x0376, 0x037e, 0x0386, 0x038e, 0x028d, 0x0393, 0x039b, 0x03a3, 0x03ab, 0x0059, 0x0059, 0x03b3, 0x03bb, 0x03c1, 0x0059, 0x03c5, 0x03cd, 0x03d5, 0x03dd, 0x028d, 0x028d, 0x028d, 0x03e1, 0x0059, 0x03e9, 0x028d, 0x03f1, 0x03f9, 0x0401, 0x0408, 0x040d, 0x028d, 0x0415, 0x0418, 0x0420, 0x0428, 0x0430, 0x0438, 0x028d, 0x0440, 0x0059, 0x0447, 0x044f, 0x0456, 0x0144, 0x045e, 0x0466, 0x046e, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0476, 0x047a, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0356, 0x0059, 0x0482, 0x048a, 0x0059, 0x0492, 0x0496, 0x049e, 0x04a6, 0x04ae, 0x04b6, 0x04be, 0x04c6, 0x04ce, 0x04d6, 0x04da, 0x04e2, 0x04ea, 0x04f1, 0x04f9, 0x0500, 0x0507, 0x050e, 0x0515, 0x051d, 0x0525, 0x052d, 0x0535, 0x053d, 0x0544, 0x0059, 0x054c, 0x0552, 0x0559, 0x0059, 0x0059, 0x055f, 0x0059, 0x0564, 0x056a, 0x0059, 0x0571, 0x0579, 0x0581, 0x0581, 0x0581, 0x0589, 0x058f, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x0597, 0x059f, 0x05a7, 0x05af, 0x05b7, 0x05bf, 0x05c7, 0x05cf, 0x05d7, 0x05df, 0x05e7, 0x05ef, 0x05f6, 0x05fe, 0x0606, 0x060e, 0x0616, 0x061e, 0x0625, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0629, 0x0059, 0x0059, 0x0631, 0x0059, 0x0638, 0x063f, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0647, 0x0059, 0x064f, 0x0656, 0x065c, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0662, 0x0059, 0x02fe, 0x0059, 0x066a, 0x0672, 0x067a, 0x067a, 0x0079, 0x0682, 0x068a, 0x0692, 0x028d, 0x069a, 0x06a1, 0x06a1, 0x06a4, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06ac, 0x06b2, 0x06ba, 0x06c2, 0x06ca, 0x06d2, 0x06da, 0x06e2, 0x06d2, 0x06ea, 0x06f2, 0x06f6, 0x06a1, 0x06a1, 0x06fb, 0x06a1, 0x06a1, 0x0702, 0x070a, 0x06a1, 0x0712, 0x06a1, 0x0716, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x071e, 0x071e, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x0726, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x072c, 0x06a1, 0x0733, 0x0456, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0738, 0x0740, 0x0059, 0x0748, 0x0750, 0x0059, 0x0059, 0x0758, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0760, 0x0768, 0x0770, 0x0778, 0x0059, 0x0780, 0x0788, 0x078b, 0x0792, 0x079a, 0x011f, 0x07a2, 0x07a9, 0x07b1, 0x07b9, 0x07bd, 0x07c5, 0x07cd, 0x028d, 0x07d4, 0x07dc, 0x07e4, 0x028d, 0x07ec, 0x07f4, 0x07fc, 0x0804, 0x080c, 0x0059, 0x0811, 0x0059, 0x0059, 0x0059, 0x0819, 0x0821, 0x0822, 0x0823, 0x0824, 0x0825, 0x0826, 0x0827, 0x0821, 0x0822, 0x0823, 0x0824, 0x0825, 0x0826, 0x0827, 0x0821, 0x0822, 0x0823, 0x0824, 0x0825, 0x0826, 0x0827, 0x0821, 0x0822, 0x0823, 0x0824, 0x0825, 0x0826, 0x0827, 0x0821, 0x0822, 0x0823, 0x0824, 0x0825, 0x0826, 0x0827, 0x0821, 0x0822, 0x0823, 0x0824, 0x0825, 0x0826, 0x0827, 0x0821, 0x0822, 0x0823, 0x0824, 0x0825, 0x0826, 0x0827, 0x0821, 0x0822, 0x0823, 0x0824, 0x0825, 0x0826, 0x0827, 0x0821, 0x0822, 0x0823, 0x0824, 0x0825, 0x0826, 0x0827, 0x0821, 0x0822, 0x0823, 0x0824, 0x0825, 0x0826, 0x0827, 0x0821, 0x0822, 0x0823, 0x0824, 0x0825, 0x0826, 0x082e, 0x0835, 0x0838, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x0840, 0x0848, 0x0850, 0x0059, 0x0059, 0x0059, 0x0858, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x085d, 0x0059, 0x0059, 0x0657, 0x0059, 0x0865, 0x0869, 0x0871, 0x0879, 0x0880, 0x0888, 0x0059, 0x0059, 0x0059, 0x088e, 0x0896, 0x089e, 0x08a6, 0x08ae, 0x08b3, 0x08bb, 0x08c3, 0x08cb, 0x00bb, 0x08d3, 0x08db, 0x028d, 0x0059, 0x0059, 0x0059, 0x0852, 0x08e3, 0x08e6, 0x0059, 0x0059, 0x08ec, 0x028c, 0x08f4, 0x08f8, 0x028d, 0x028d, 0x028d, 0x028d, 0x0900, 0x0059, 0x0903, 0x090b, 0x0059, 0x0911, 0x0144, 0x0915, 0x091d, 0x0059, 0x0925, 0x028d, 0x0059, 0x0059, 0x0059, 0x0059, 0x048a, 0x03af, 0x092d, 0x0933, 0x0059, 0x0938, 0x0059, 0x093f, 0x0943, 0x0948, 0x0059, 0x0950, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0672, 0x03c5, 0x0953, 0x095b, 0x095f, 0x028d, 0x028d, 0x0967, 0x096a, 0x0972, 0x0059, 0x03cd, 0x097a, 0x028d, 0x0982, 0x0989, 0x0991, 0x028d, 0x028d, 0x0059, 0x0999, 0x0657, 0x0059, 0x09a1, 0x09a8, 0x09b0, 0x0059, 0x0059, 0x028d, 0x0059, 0x09b8, 0x0059, 0x09c0, 0x048c, 0x09c8, 0x09ce, 0x09d6, 0x028d, 0x028d, 0x0059, 0x0059, 0x09de, 0x028d, 0x0059, 0x0854, 0x0059, 0x09e6, 0x0059, 0x09ed, 0x011f, 0x09f5, 0x09fc, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x03cd, 0x0059, 0x0a04, 0x0a0c, 0x0a0e, 0x0059, 0x0938, 0x0a16, 0x08f4, 0x0a1e, 0x08f4, 0x0952, 0x0672, 0x0a26, 0x0a28, 0x0a2f, 0x0a36, 0x0430, 0x0a3e, 0x0a46, 0x0a4c, 0x0a54, 0x0a5b, 0x0a63, 0x0a67, 0x0430, 0x0a6f, 0x0a77, 0x0a7f, 0x065d, 0x0a87, 0x0a8f, 0x028d, 0x0a97, 0x0a9f, 0x0a55, 0x0aa7, 0x0aaf, 0x0ab1, 0x0ab9, 0x0ac1, 0x028d, 0x0ac7, 0x0acf, 0x0ad7, 0x0059, 0x0adf, 0x0ae7, 0x0aef, 0x0059, 0x0af7, 0x0aff, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x0059, 0x0b07, 0x0b0f, 0x028d, 0x0059, 0x0b17, 0x0b1f, 0x0b27, 0x0059, 0x0b2f, 0x0b37, 0x0b3e, 0x0b3f, 0x0b47, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x0059, 0x0b4f, 0x028d, 0x028d, 0x028d, 0x0059, 0x0059, 0x0b57, 0x028d, 0x0b5f, 0x0b67, 0x028d, 0x028d, 0x0659, 0x0b6f, 0x0b77, 0x0b7f, 0x0b83, 0x0b8b, 0x0059, 0x0b92, 0x0b9a, 0x0059, 0x03b3, 0x0ba2, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x0059, 0x0baa, 0x0bb2, 0x0bb7, 0x0bbf, 0x0bc6, 0x0bcb, 0x0bd1, 0x028d, 0x028d, 0x0bd9, 0x0bdd, 0x0be5, 0x0bed, 0x0bf3, 0x0bfb, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x0bff, 0x0c07, 0x0c0a, 0x0c12, 0x028d, 0x028d, 0x0c19, 0x0c21, 0x0c29, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x034e, 0x028d, 0x028d, 0x028d, 0x0059, 0x0059, 0x0059, 0x0c31, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0c39, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x08f4, 0x0059, 0x0059, 0x0854, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0c41, 0x0059, 0x0c49, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0c4c, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0c53, 0x0c5b, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0852, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0c63, 0x0059, 0x0059, 0x0059, 0x0c6a, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x0c6c, 0x0c74, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x03b3, 0x03cd, 0x0c7c, 0x0059, 0x03cd, 0x03af, 0x0c81, 0x0059, 0x0c89, 0x0c90, 0x0c98, 0x0951, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x0059, 0x0ca0, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x0059, 0x0059, 0x0ca8, 0x028d, 0x028d, 0x028d, 0x0059, 0x0059, 0x0cb0, 0x0cb5, 0x0cbb, 0x028d, 0x028d, 0x0cc3, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a3, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x071e, 0x071e, 0x071e, 0x071e, 0x071e, 0x071e, 0x071e, 0x071e, 0x071e, 0x071e, 0x071e, 0x071e, 0x071e, 0x071e, 0x0ccb, 0x0cd1, 0x0cd9, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x0cdd, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x0ce5, 0x0cea, 0x0cf1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a2, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x0059, 0x0059, 0x0059, 0x0cf9, 0x0cfe, 0x0d06, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0d0e, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0950, 0x028d, 0x028d, 0x0079, 0x0d16, 0x0d1d, 0x0059, 0x0059, 0x0059, 0x0c39, 0x028d, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x03c5, 0x0059, 0x0d24, 0x0059, 0x0d2b, 0x0d33, 0x0d39, 0x0059, 0x0579, 0x0059, 0x0059, 0x0d41, 0x028d, 0x028d, 0x028d, 0x0950, 0x0950, 0x071e, 0x071e, 0x0d49, 0x0d51, 0x028d, 0x028d, 0x028d, 0x028d, 0x0059, 0x0059, 0x0492, 0x0059, 0x0d59, 0x0d61, 0x0d69, 0x0059, 0x0d70, 0x0d6b, 0x0d78, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0d7f, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0d84, 0x0d88, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0079, 0x0d90, 0x0079, 0x0d97, 0x0d9e, 0x0da6, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x03cd, 0x0dad, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x0db5, 0x0dbd, 0x0059, 0x0dc2, 0x0dc7, 0x028d, 0x028d, 0x028d, 0x0059, 0x0dcf, 0x0dd7, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x08f4, 0x0ddf, 0x0059, 0x0de7, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x08f4, 0x0def, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x08f4, 0x0df7, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x0dff, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0e07, 0x028d, 0x0059, 0x0059, 0x0e0f, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x0e17, 0x0059, 0x0e1c, 0x028d, 0x028d, 0x0e24, 0x048a, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x0d69, 0x0e2c, 0x0e34, 0x0e3c, 0x0e44, 0x0e4c, 0x028d, 0x0ba6, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x028d, 0x0e54, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e5b, 0x0e56, 0x0e63, 0x0e68, 0x0581, 0x0e6e, 0x0e76, 0x0e7d, 0x0e56, 0x0e84, 0x0e8c, 0x0e93, 0x0e9b, 0x0ea3, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0eab, 0x0eb3, 0x0eab, 0x0eb9, 0x0ec1, 0x0ec9, 0x0ed1, 0x0ed9, 0x0eab, 0x0ee1, 0x0ee9, 0x0eab, 0x0eab, 0x0ef1, 0x0eab, 0x0ef6, 0x0efe, 0x0f05, 0x0f0d, 0x0f13, 0x0f1a, 0x0e54, 0x0f20, 0x0f27, 0x0eab, 0x0eab, 0x0f2e, 0x0f32, 0x0eab, 0x0eab, 0x0f3a, 0x0f42, 0x0059, 0x0059, 0x0059, 0x0f4a, 0x0059, 0x0059, 0x0f52, 0x0f5a, 0x0f62, 0x0059, 0x0f68, 0x0059, 0x0f70, 0x0f75, 0x0f7d, 0x0f7e, 0x0f86, 0x0f89, 0x0f90, 0x0eab, 0x0eab, 0x0eab, 0x0eab, 0x0eab, 0x0f98, 0x0f98, 0x0f9b, 0x0fa0, 0x0fa8, 0x0eab, 0x0faf, 0x0fb7, 0x0059, 0x0059, 0x0059, 0x0059, 0x0fbf, 0x0059, 0x0059, 0x0d0e, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0e56, 0x0fc7, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x06a1, 0x0fcf, 0x0fd7, 0x0079, 0x0079, 0x0079, 0x0020, 0x0020, 0x0020, 0x0020, 0x0079, 0x0079, 0x0079, 0x0079, 0x0079, 0x0079, 0x0079, 0x0fdf, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0581, 0x0fe7, ]; #[rustfmt::skip] const STAGE2: [u16; 4079] = [ 0x0000, 0x0004, 0x0008, 0x000c, 0x0010, 0x0014, 0x0018, 0x001c, 0x0020, 0x0024, 0x0028, 0x002c, 0x0030, 0x0034, 0x0038, 0x003c, 0x0040, 0x0044, 0x0048, 0x004c, 0x0050, 0x0054, 0x0058, 0x005c, 0x0060, 0x0064, 0x0068, 0x006c, 0x0070, 0x0074, 0x0078, 0x007c, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0080, 0x0084, 0x0087, 0x008b, 0x008f, 0x0093, 0x0095, 0x0099, 0x0040, 0x009d, 0x0040, 0x0040, 0x009f, 0x00a0, 0x009f, 0x00a4, 0x00a6, 0x009d, 0x00aa, 0x00a6, 0x00ac, 0x00a0, 0x00aa, 0x00af, 0x009e, 0x0040, 0x0040, 0x0040, 0x00b0, 0x0040, 0x00b4, 0x0040, 0x00a4, 0x00b4, 0x0040, 0x00a9, 0x0040, 0x009f, 0x00b4, 0x00aa, 0x009f, 0x00b7, 0x009e, 0x00a4, 0x0040, 0x0040, 0x0040, 0x00a4, 0x00b4, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x009d, 0x00af, 0x00af, 0x00af, 0x009f, 0x0040, 0x0040, 0x0040, 0x0040, 0x009e, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00ba, 0x00be, 0x00c2, 0x00c3, 0x0040, 0x00c7, 0x00cb, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x00d0, 0x00cf, 0x00cf, 0x00cf, 0x00d3, 0x00d4, 0x00cf, 0x00cf, 0x00cf, 0x0040, 0x0040, 0x00d8, 0x00da, 0x00de, 0x0040, 0x00e2, 0x00e4, 0x00a9, 0x00b7, 0x00b7, 0x00b7, 0x00e8, 0x00b7, 0x00a6, 0x0040, 0x00a9, 0x00b7, 0x00b7, 0x00b7, 0x00ab, 0x00b7, 0x00a6, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x009e, 0x0040, 0x0040, 0x0040, 0x00b7, 0x00b7, 0x00b7, 0x00b7, 0x00b7, 0x00b7, 0x00b7, 0x00b7, 0x009e, 0x0040, 0x0040, 0x0040, 0x00ec, 0x00cf, 0x00ef, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e1, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e2, 0x00e1, 0x0040, 0x0040, 0x00f2, 0x00f5, 0x00f9, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x00fb, 0x00ee, 0x00fe, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e2, 0x00df, 0x0040, 0x00dd, 0x00de, 0x00de, 0x0102, 0x0104, 0x0107, 0x003a, 0x00cf, 0x00cf, 0x010b, 0x010d, 0x0040, 0x0040, 0x00ec, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x0030, 0x0030, 0x0111, 0x0114, 0x0118, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x011c, 0x00cf, 0x011f, 0x00cf, 0x0122, 0x0125, 0x00ef, 0x0030, 0x0030, 0x0129, 0x0040, 0x0040, 0x0040, 0x012b, 0x0117, 0x0040, 0x0040, 0x0040, 0x0040, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x012f, 0x00e1, 0x0040, 0x0040, 0x0040, 0x0040, 0x00ed, 0x00cf, 0x00cf, 0x0133, 0x00de, 0x00de, 0x00de, 0x0030, 0x0030, 0x0129, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00ec, 0x00cf, 0x00cf, 0x0040, 0x0137, 0x013a, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00ed, 0x013e, 0x00cf, 0x0140, 0x0140, 0x0142, 0x0040, 0x0040, 0x0040, 0x00e2, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0140, 0x0144, 0x0040, 0x0040, 0x00e2, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e2, 0x0148, 0x014a, 0x00cf, 0x00cf, 0x0040, 0x0040, 0x00ed, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x014d, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x0150, 0x0040, 0x0040, 0x0040, 0x0040, 0x0154, 0x0155, 0x0155, 0x0155, 0x0155, 0x0155, 0x0155, 0x0157, 0x015b, 0x015e, 0x00cf, 0x0161, 0x0164, 0x0140, 0x00cf, 0x0155, 0x0155, 0x00ed, 0x0168, 0x0030, 0x0030, 0x0040, 0x0040, 0x0155, 0x0155, 0x016c, 0x00e1, 0x0040, 0x0170, 0x0170, 0x0154, 0x0155, 0x0155, 0x0174, 0x0155, 0x0177, 0x017a, 0x017c, 0x015b, 0x015e, 0x0180, 0x0183, 0x0186, 0x00de, 0x0189, 0x00de, 0x0176, 0x00ed, 0x018d, 0x0030, 0x0030, 0x0191, 0x0040, 0x0195, 0x0199, 0x019c, 0x00e1, 0x00e2, 0x00df, 0x0170, 0x0040, 0x0040, 0x0040, 0x00e4, 0x0040, 0x00e4, 0x01a0, 0x01a1, 0x01a5, 0x01a8, 0x014a, 0x01aa, 0x0142, 0x017f, 0x00de, 0x00e1, 0x01ae, 0x00de, 0x018d, 0x0030, 0x0030, 0x00ef, 0x01b2, 0x00de, 0x00de, 0x019c, 0x00e1, 0x0040, 0x00e3, 0x00e3, 0x0154, 0x0155, 0x0155, 0x0174, 0x0155, 0x0174, 0x01b5, 0x017c, 0x015b, 0x015e, 0x0130, 0x01b9, 0x01bc, 0x00dd, 0x00de, 0x00de, 0x00de, 0x00ed, 0x018d, 0x0030, 0x0030, 0x01c0, 0x00de, 0x01c3, 0x00cf, 0x01c7, 0x00e1, 0x0040, 0x0170, 0x0170, 0x0154, 0x0155, 0x0155, 0x0174, 0x0155, 0x0174, 0x01b5, 0x017c, 0x01cb, 0x015e, 0x0180, 0x0183, 0x01bc, 0x00de, 0x019c, 0x00de, 0x0176, 0x00ed, 0x018d, 0x0030, 0x0030, 0x01cf, 0x0040, 0x00de, 0x00de, 0x01ab, 0x00e1, 0x00e2, 0x00d8, 0x00e4, 0x01a1, 0x01a0, 0x00e4, 0x00df, 0x00dd, 0x00e2, 0x00d8, 0x0040, 0x0040, 0x01a1, 0x01d3, 0x01d7, 0x01d3, 0x01d9, 0x01dc, 0x00dd, 0x0189, 0x00de, 0x00de, 0x018d, 0x0030, 0x0030, 0x0040, 0x0040, 0x01e0, 0x00de, 0x0161, 0x0118, 0x0040, 0x00e4, 0x00e4, 0x0154, 0x0155, 0x0155, 0x0174, 0x0155, 0x0155, 0x0155, 0x017c, 0x0125, 0x0161, 0x01e4, 0x019b, 0x01e7, 0x00de, 0x01ea, 0x01ee, 0x01f1, 0x00ed, 0x018d, 0x0030, 0x0030, 0x00de, 0x01f3, 0x0040, 0x0040, 0x016c, 0x01f6, 0x0040, 0x00e4, 0x00e4, 0x0040, 0x0040, 0x0040, 0x00e4, 0x0040, 0x0040, 0x00e1, 0x01a1, 0x01cb, 0x01fa, 0x01fd, 0x01d9, 0x0142, 0x00de, 0x0201, 0x00de, 0x01a0, 0x00ed, 0x018d, 0x0030, 0x0030, 0x0204, 0x00de, 0x00de, 0x00de, 0x0160, 0x0040, 0x0040, 0x00e4, 0x00e4, 0x0154, 0x0155, 0x0155, 0x0155, 0x0155, 0x0155, 0x0155, 0x0156, 0x015b, 0x015e, 0x01a5, 0x01d9, 0x0207, 0x00de, 0x01f7, 0x0040, 0x0040, 0x00ed, 0x018d, 0x0030, 0x0030, 0x0040, 0x0040, 0x020a, 0x0040, 0x01c7, 0x00e1, 0x0040, 0x0040, 0x0040, 0x00e2, 0x00d8, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e3, 0x0040, 0x0040, 0x01f1, 0x0040, 0x00e2, 0x017e, 0x0189, 0x015d, 0x020e, 0x01fa, 0x01fa, 0x00de, 0x018d, 0x0030, 0x0030, 0x01d3, 0x00dd, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x01a4, 0x00cf, 0x012f, 0x0211, 0x00de, 0x014a, 0x00cf, 0x0215, 0x0030, 0x0030, 0x0219, 0x00de, 0x00de, 0x00de, 0x00de, 0x01a4, 0x00cf, 0x00cf, 0x0210, 0x00de, 0x00de, 0x00cf, 0x012f, 0x0030, 0x0030, 0x021d, 0x00de, 0x0221, 0x0224, 0x0228, 0x022c, 0x022e, 0x003f, 0x00ef, 0x0040, 0x0030, 0x0030, 0x0129, 0x0040, 0x0040, 0x0232, 0x0234, 0x0236, 0x0040, 0x0040, 0x0040, 0x00dd, 0x00f9, 0x00cf, 0x00cf, 0x023a, 0x00cf, 0x00fc, 0x0040, 0x0140, 0x00cf, 0x00cf, 0x00f9, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x023e, 0x0040, 0x0116, 0x0040, 0x00e4, 0x0242, 0x0040, 0x0246, 0x00de, 0x00de, 0x00de, 0x00f9, 0x024a, 0x00cf, 0x019c, 0x01a8, 0x0030, 0x0030, 0x0219, 0x0040, 0x00de, 0x01d3, 0x0142, 0x014b, 0x0210, 0x00de, 0x00de, 0x00de, 0x00f9, 0x0210, 0x00de, 0x00de, 0x017e, 0x01a8, 0x00de, 0x017f, 0x0030, 0x0030, 0x021d, 0x017f, 0x0040, 0x00e3, 0x00de, 0x01f1, 0x0040, 0x0040, 0x0040, 0x0040, 0x024e, 0x024e, 0x024e, 0x024e, 0x024e, 0x024e, 0x024e, 0x024e, 0x0252, 0x0252, 0x0252, 0x0252, 0x0252, 0x0252, 0x0252, 0x0252, 0x0256, 0x0256, 0x0256, 0x0256, 0x0256, 0x0256, 0x0256, 0x0256, 0x0040, 0x0040, 0x00e4, 0x01a1, 0x0040, 0x00e2, 0x00e4, 0x01a1, 0x0040, 0x0040, 0x00e4, 0x01a1, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e4, 0x01a1, 0x0040, 0x00e2, 0x00e4, 0x01a1, 0x0040, 0x0040, 0x0040, 0x00e2, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e4, 0x01a1, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e2, 0x00f9, 0x025a, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00dd, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x01a1, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x01a1, 0x0040, 0x01a1, 0x025b, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x025b, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0058, 0x025f, 0x0040, 0x0040, 0x0263, 0x0266, 0x0040, 0x0040, 0x00dd, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x00ed, 0x026a, 0x00de, 0x00df, 0x0040, 0x0040, 0x0040, 0x0040, 0x00ed, 0x026e, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x00ed, 0x00de, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x00e4, 0x0272, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x0274, 0x00cf, 0x0160, 0x01fa, 0x01d5, 0x015e, 0x00cf, 0x00cf, 0x0278, 0x027c, 0x017f, 0x0030, 0x0030, 0x021d, 0x00de, 0x0040, 0x0040, 0x01a1, 0x00de, 0x0280, 0x0284, 0x0288, 0x028b, 0x0030, 0x0030, 0x021d, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00dd, 0x00de, 0x0040, 0x00ee, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x01b2, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x01a1, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e2, 0x0150, 0x028f, 0x0161, 0x00de, 0x01d5, 0x01fa, 0x015e, 0x00de, 0x00dd, 0x010f, 0x0030, 0x0030, 0x00de, 0x00de, 0x00de, 0x00de, 0x0030, 0x0030, 0x0293, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00ec, 0x01c8, 0x00d8, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x0296, 0x00cf, 0x012f, 0x020e, 0x00f9, 0x00cf, 0x0161, 0x028f, 0x00cf, 0x00cf, 0x01aa, 0x0030, 0x0030, 0x021d, 0x00de, 0x0030, 0x0030, 0x021d, 0x00de, 0x00de, 0x00de, 0x00de, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x012f, 0x00de, 0x00de, 0x00de, 0x00de, 0x00cf, 0x0299, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x024a, 0x0150, 0x0161, 0x01d5, 0x0299, 0x00de, 0x029b, 0x00de, 0x00de, 0x029b, 0x029f, 0x02a2, 0x02a3, 0x02a4, 0x00cf, 0x00cf, 0x02a3, 0x02a3, 0x029f, 0x0151, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x02a8, 0x0160, 0x0274, 0x00ef, 0x0030, 0x0030, 0x0129, 0x0040, 0x00de, 0x02ac, 0x0160, 0x02af, 0x0160, 0x00de, 0x00de, 0x0040, 0x01fa, 0x01fa, 0x00cf, 0x00cf, 0x015d, 0x029a, 0x02b3, 0x0030, 0x0030, 0x021d, 0x00e1, 0x0030, 0x0030, 0x0129, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0264, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e2, 0x00e1, 0x0040, 0x0040, 0x00de, 0x00de, 0x0215, 0x00cf, 0x00cf, 0x00cf, 0x024a, 0x00cf, 0x0118, 0x0117, 0x0040, 0x02b7, 0x02bb, 0x00de, 0x00cf, 0x00cf, 0x00cf, 0x02bf, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x02c0, 0x0040, 0x01a1, 0x0040, 0x01a1, 0x0040, 0x0040, 0x01af, 0x01af, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x01a1, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e4, 0x0040, 0x0040, 0x0040, 0x00d8, 0x0040, 0x00e1, 0x0040, 0x0040, 0x0040, 0x0040, 0x00d8, 0x00e4, 0x0040, 0x02c4, 0x02b3, 0x02c8, 0x02cc, 0x02d0, 0x02d4, 0x00c8, 0x02d8, 0x02d8, 0x02dc, 0x02e0, 0x02e4, 0x02e6, 0x02ea, 0x02ee, 0x02f2, 0x02f6, 0x0040, 0x02fa, 0x02fd, 0x0040, 0x0040, 0x02ff, 0x02b3, 0x0303, 0x0307, 0x030a, 0x00cf, 0x00cf, 0x01a1, 0x00c3, 0x0040, 0x030e, 0x0094, 0x00c3, 0x0040, 0x0312, 0x0040, 0x0040, 0x0040, 0x00dd, 0x0316, 0x0317, 0x0316, 0x031b, 0x0316, 0x031d, 0x0317, 0x031d, 0x031f, 0x0316, 0x0316, 0x0316, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x0210, 0x00de, 0x00de, 0x00de, 0x0323, 0x00a2, 0x0325, 0x0040, 0x00a0, 0x0327, 0x0040, 0x0040, 0x032a, 0x009d, 0x00a0, 0x0040, 0x0040, 0x0040, 0x032d, 0x0040, 0x0040, 0x0040, 0x0040, 0x0331, 0x0334, 0x0331, 0x00c8, 0x00c7, 0x00c7, 0x00c7, 0x0040, 0x00c7, 0x00c7, 0x0338, 0x0040, 0x0040, 0x00a2, 0x00de, 0x00c7, 0x033c, 0x033e, 0x0040, 0x0040, 0x0341, 0x0040, 0x0040, 0x0040, 0x00a6, 0x0040, 0x0040, 0x0040, 0x0040, 0x00a1, 0x00c3, 0x0040, 0x0040, 0x00b4, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0345, 0x00a0, 0x0348, 0x00a0, 0x034a, 0x00a2, 0x00a1, 0x0094, 0x0348, 0x0344, 0x00c7, 0x00ca, 0x0040, 0x00c7, 0x0040, 0x0338, 0x0040, 0x0040, 0x00c3, 0x00c3, 0x00a1, 0x0040, 0x0040, 0x0040, 0x0338, 0x00c7, 0x00c5, 0x00c5, 0x0040, 0x0040, 0x0040, 0x0040, 0x00c5, 0x00c5, 0x0040, 0x0040, 0x0040, 0x00a2, 0x00a2, 0x0040, 0x00a2, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00a0, 0x0040, 0x0040, 0x0040, 0x034e, 0x0040, 0x0040, 0x0040, 0x0040, 0x0352, 0x0040, 0x00a1, 0x0040, 0x0356, 0x0040, 0x0040, 0x035a, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x035e, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x035f, 0x0040, 0x0040, 0x0040, 0x0040, 0x0363, 0x0366, 0x036a, 0x0040, 0x036e, 0x0040, 0x0040, 0x01a1, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x0040, 0x0040, 0x00e2, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00c7, 0x00c7, 0x00c7, 0x00c7, 0x00c7, 0x00c7, 0x00c7, 0x00c7, 0x0372, 0x00c7, 0x00c7, 0x00c7, 0x00c7, 0x00c7, 0x00c7, 0x00c7, 0x0375, 0x00c7, 0x00c7, 0x00c7, 0x00c7, 0x0378, 0x00c9, 0x00c7, 0x037c, 0x0040, 0x00c5, 0x0380, 0x0040, 0x0338, 0x0382, 0x00c5, 0x0348, 0x00c5, 0x0338, 0x0040, 0x0040, 0x0040, 0x00c5, 0x0338, 0x0040, 0x00a0, 0x0040, 0x0040, 0x035f, 0x0386, 0x038a, 0x038e, 0x0391, 0x0393, 0x036e, 0x0397, 0x039b, 0x039f, 0x03a3, 0x03a3, 0x03a3, 0x03a3, 0x03a7, 0x03a7, 0x03ab, 0x03a3, 0x03af, 0x03a3, 0x03a7, 0x03a7, 0x03a7, 0x03a3, 0x03a3, 0x03a3, 0x03b3, 0x03b3, 0x03b7, 0x03b3, 0x03a3, 0x03a3, 0x03a3, 0x0367, 0x03a3, 0x037e, 0x03bb, 0x03bd, 0x03a4, 0x03a3, 0x03a3, 0x0393, 0x03c1, 0x03a3, 0x03a5, 0x03a3, 0x03a3, 0x03a3, 0x03a3, 0x03c4, 0x038a, 0x03c5, 0x03c8, 0x03cb, 0x03ce, 0x03d2, 0x03c7, 0x03d6, 0x03d9, 0x03a3, 0x03dc, 0x033c, 0x03df, 0x03e3, 0x03e6, 0x03e9, 0x038a, 0x03ed, 0x03f1, 0x03f5, 0x036e, 0x03f8, 0x0040, 0x032d, 0x0040, 0x03fc, 0x0040, 0x035f, 0x035e, 0x0040, 0x009e, 0x0040, 0x0400, 0x0040, 0x0404, 0x0407, 0x040a, 0x040e, 0x0411, 0x0414, 0x03a2, 0x0352, 0x0352, 0x0352, 0x0418, 0x00c7, 0x00c7, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x0363, 0x0040, 0x0040, 0x032d, 0x0040, 0x0040, 0x0040, 0x03fc, 0x0040, 0x0040, 0x0407, 0x0040, 0x041c, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x041f, 0x0352, 0x0352, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x037e, 0x0040, 0x0040, 0x0058, 0x0422, 0x0422, 0x0422, 0x0422, 0x0422, 0x0426, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0352, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0354, 0x0040, 0x0429, 0x0040, 0x0040, 0x0040, 0x0040, 0x0407, 0x03fc, 0x0040, 0x0040, 0x0040, 0x0040, 0x03fc, 0x042d, 0x0338, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00d8, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e3, 0x0040, 0x0040, 0x0040, 0x00ec, 0x00ef, 0x00de, 0x0431, 0x0434, 0x0040, 0x0040, 0x00de, 0x00df, 0x0437, 0x00de, 0x00de, 0x014a, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e2, 0x00de, 0x00de, 0x0040, 0x00e2, 0x0040, 0x00e2, 0x0040, 0x00e2, 0x0040, 0x00e2, 0x0411, 0x0411, 0x0411, 0x043b, 0x02b3, 0x043d, 0x0441, 0x0445, 0x0449, 0x0352, 0x044b, 0x044d, 0x043d, 0x025b, 0x01a1, 0x0451, 0x0455, 0x02b3, 0x0451, 0x0453, 0x003c, 0x0459, 0x045b, 0x045f, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x0465, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x00de, 0x00de, 0x00de, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x0468, 0x00de, 0x00de, 0x00de, 0x00de, 0x0463, 0x0463, 0x0463, 0x0463, 0x046c, 0x046f, 0x0473, 0x0473, 0x0475, 0x0473, 0x0473, 0x0479, 0x0463, 0x0463, 0x047d, 0x047f, 0x0483, 0x0486, 0x0488, 0x048b, 0x048f, 0x0491, 0x0486, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x0484, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x0484, 0x0491, 0x0463, 0x0485, 0x0463, 0x0493, 0x0496, 0x0499, 0x049d, 0x0491, 0x0486, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x0484, 0x0491, 0x0463, 0x0485, 0x0463, 0x049f, 0x0488, 0x04a3, 0x00de, 0x0462, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x0462, 0x0463, 0x0463, 0x0463, 0x0464, 0x0463, 0x0463, 0x0463, 0x0463, 0x0468, 0x00de, 0x04a7, 0x04ab, 0x04ab, 0x04ab, 0x04ab, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x0464, 0x0463, 0x0463, 0x00c7, 0x00c7, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x04af, 0x04b1, 0x0463, 0x03bd, 0x03bd, 0x03bd, 0x03bd, 0x03bd, 0x03bd, 0x03bd, 0x03bd, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x046f, 0x0463, 0x0463, 0x0463, 0x04a6, 0x0463, 0x0463, 0x0463, 0x0463, 0x0464, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x04b5, 0x0040, 0x0040, 0x0040, 0x0040, 0x0030, 0x0030, 0x0129, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x00ec, 0x0215, 0x00cf, 0x00cf, 0x00ef, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00ed, 0x0040, 0x0040, 0x0040, 0x0040, 0x04b9, 0x02b3, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x01a1, 0x00e3, 0x00e1, 0x0040, 0x00dd, 0x00de, 0x00de, 0x00de, 0x00de, 0x00d8, 0x0040, 0x0040, 0x0040, 0x0116, 0x0116, 0x00ec, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x01f7, 0x04bd, 0x0040, 0x0210, 0x0040, 0x0040, 0x04c1, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x04c5, 0x00de, 0x00de, 0x04c9, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x01fa, 0x01fa, 0x01fa, 0x0142, 0x00de, 0x029b, 0x0030, 0x0030, 0x021d, 0x00de, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x00ef, 0x0040, 0x0040, 0x04cd, 0x0040, 0x00ed, 0x00cf, 0x04d0, 0x0040, 0x0040, 0x0040, 0x0040, 0x00ec, 0x00cf, 0x00cf, 0x0160, 0x00de, 0x00de, 0x00df, 0x024e, 0x024e, 0x024e, 0x024e, 0x024e, 0x024e, 0x024e, 0x04d4, 0x0150, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x014a, 0x015d, 0x0160, 0x0160, 0x04d8, 0x04d9, 0x02a1, 0x04dd, 0x00de, 0x00de, 0x00de, 0x04e1, 0x00de, 0x017f, 0x00de, 0x00de, 0x0030, 0x0030, 0x021d, 0x00de, 0x00de, 0x00f9, 0x0150, 0x04bd, 0x01a8, 0x00de, 0x00de, 0x02b4, 0x02b3, 0x02b3, 0x026a, 0x00de, 0x00de, 0x00de, 0x029f, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x0210, 0x00de, 0x00de, 0x00de, 0x00de, 0x019b, 0x01aa, 0x0210, 0x014b, 0x017f, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x0040, 0x0040, 0x01f7, 0x0160, 0x0266, 0x04e5, 0x00de, 0x00de, 0x00e1, 0x00e2, 0x00e1, 0x00e2, 0x00e1, 0x00e2, 0x00de, 0x00de, 0x0040, 0x00e2, 0x0040, 0x00e2, 0x0040, 0x0040, 0x0040, 0x0040, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x01f7, 0x01d6, 0x04e9, 0x01dc, 0x0030, 0x0030, 0x021d, 0x00de, 0x04ed, 0x04ee, 0x04ee, 0x04ee, 0x04ee, 0x04ee, 0x04ee, 0x04ed, 0x04ee, 0x04ee, 0x04ee, 0x04ee, 0x04ee, 0x04ee, 0x00de, 0x00de, 0x00de, 0x0252, 0x0252, 0x0252, 0x0252, 0x04f2, 0x04f5, 0x0256, 0x0256, 0x0256, 0x0256, 0x0256, 0x0256, 0x0256, 0x00de, 0x0040, 0x00e2, 0x00de, 0x00de, 0x00df, 0x0040, 0x00de, 0x01b1, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e2, 0x0040, 0x01ae, 0x00e3, 0x00e4, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e2, 0x00de, 0x00de, 0x00de, 0x00df, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x04f9, 0x0040, 0x0040, 0x00de, 0x00df, 0x00de, 0x00de, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x04fd, 0x00cf, 0x00cf, 0x00cf, 0x0501, 0x0505, 0x0508, 0x050c, 0x00de, 0x0510, 0x0512, 0x0511, 0x0513, 0x0463, 0x0472, 0x0517, 0x0517, 0x051b, 0x051f, 0x0463, 0x0523, 0x0527, 0x0472, 0x0474, 0x0463, 0x0464, 0x052b, 0x00de, 0x0040, 0x00e4, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x052f, 0x0533, 0x0537, 0x0475, 0x053b, 0x0463, 0x0463, 0x053e, 0x0542, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x0546, 0x053c, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x0546, 0x054a, 0x054e, 0x0551, 0x00de, 0x00de, 0x0554, 0x02a3, 0x02a3, 0x02a3, 0x02a3, 0x02a3, 0x02a3, 0x02a3, 0x0556, 0x02a3, 0x02a3, 0x02a3, 0x02a3, 0x02a3, 0x02a3, 0x02a3, 0x055a, 0x04e1, 0x02a3, 0x04e1, 0x02a3, 0x04e1, 0x02a3, 0x04e1, 0x055c, 0x0560, 0x0563, 0x0040, 0x00e2, 0x0000, 0x0000, 0x02e5, 0x0333, 0x0040, 0x00e2, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e2, 0x00e3, 0x0040, 0x0040, 0x0040, 0x01a1, 0x0040, 0x0040, 0x0040, 0x01a1, 0x0567, 0x00df, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00df, 0x0040, 0x0040, 0x0040, 0x00e2, 0x0040, 0x0040, 0x0040, 0x00dd, 0x00de, 0x00de, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x056b, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00dd, 0x00de, 0x00de, 0x00de, 0x0118, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00de, 0x00de, 0x00e1, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00ed, 0x012f, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x056f, 0x0040, 0x00de, 0x0040, 0x0040, 0x025b, 0x01a1, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x00de, 0x00de, 0x00df, 0x0040, 0x0040, 0x00e2, 0x0040, 0x00e2, 0x00e3, 0x0040, 0x0040, 0x0040, 0x00e3, 0x0040, 0x00e3, 0x00dd, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x0040, 0x00e3, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e4, 0x0040, 0x00e2, 0x00de, 0x0040, 0x01a1, 0x00e4, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e3, 0x00dd, 0x0170, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x056f, 0x0040, 0x0040, 0x00de, 0x00df, 0x0040, 0x0040, 0x00de, 0x00de, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e2, 0x01a1, 0x00df, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x029a, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x01a1, 0x00df, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00de, 0x0040, 0x0140, 0x01ea, 0x00de, 0x00cf, 0x0040, 0x00e1, 0x00e1, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x01a1, 0x012f, 0x014a, 0x0040, 0x0040, 0x00dd, 0x00de, 0x02b3, 0x02b3, 0x00dd, 0x00de, 0x0040, 0x0573, 0x00df, 0x0040, 0x02b3, 0x0577, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x01a1, 0x02c7, 0x02b3, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e2, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x01a1, 0x00de, 0x00e1, 0x00dd, 0x00de, 0x00de, 0x00e1, 0x0040, 0x00de, 0x00de, 0x00de, 0x00de, 0x0040, 0x0040, 0x00dd, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e2, 0x00de, 0x00d8, 0x0040, 0x00cf, 0x00de, 0x00de, 0x0030, 0x0030, 0x021d, 0x00de, 0x0040, 0x01a1, 0x00f9, 0x057b, 0x0040, 0x0040, 0x0040, 0x0040, 0x01a1, 0x00de, 0x00d8, 0x00de, 0x00de, 0x00de, 0x00de, 0x0040, 0x0040, 0x057e, 0x0581, 0x01a1, 0x00de, 0x00de, 0x00de, 0x00d8, 0x00dd, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00cf, 0x0040, 0x00ed, 0x00cf, 0x00cf, 0x0118, 0x0040, 0x01a1, 0x00de, 0x00ed, 0x00ef, 0x01a1, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x0297, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00cf, 0x00cf, 0x00fa, 0x02a2, 0x055b, 0x04e1, 0x02a3, 0x02a3, 0x02a3, 0x055b, 0x00de, 0x00de, 0x01aa, 0x0210, 0x00de, 0x0583, 0x0040, 0x0040, 0x0040, 0x0040, 0x028f, 0x0150, 0x02ba, 0x0587, 0x0589, 0x00de, 0x00de, 0x058c, 0x0040, 0x0040, 0x0040, 0x0040, 0x00dd, 0x00de, 0x0030, 0x0030, 0x021d, 0x00de, 0x0215, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00ec, 0x00cf, 0x015e, 0x00cf, 0x0590, 0x0030, 0x0030, 0x02b3, 0x0594, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x00ec, 0x02c4, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x01f7, 0x015d, 0x00cf, 0x0150, 0x0596, 0x0265, 0x059a, 0x01cb, 0x0030, 0x0030, 0x059e, 0x0303, 0x00e1, 0x0040, 0x0040, 0x0040, 0x0040, 0x00dd, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x028f, 0x0160, 0x024a, 0x043d, 0x05a2, 0x056b, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x0040, 0x00e2, 0x00e4, 0x00e3, 0x0040, 0x0040, 0x0040, 0x00e3, 0x0040, 0x0040, 0x05a5, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x028f, 0x00cf, 0x012f, 0x00de, 0x0030, 0x0030, 0x021d, 0x00de, 0x0160, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x014a, 0x05a9, 0x0161, 0x0183, 0x0183, 0x05ab, 0x00de, 0x0189, 0x00de, 0x04df, 0x01d3, 0x014b, 0x00cf, 0x0210, 0x00cf, 0x0210, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x05ad, 0x028f, 0x00cf, 0x05b1, 0x05b2, 0x01fb, 0x01d5, 0x05b6, 0x05b9, 0x055c, 0x00de, 0x01ea, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x01f9, 0x00cf, 0x00cf, 0x015d, 0x0159, 0x0263, 0x0451, 0x0030, 0x0030, 0x0219, 0x01b1, 0x01a1, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x028f, 0x00cf, 0x02ae, 0x028f, 0x024a, 0x0040, 0x00de, 0x00de, 0x0030, 0x0030, 0x021d, 0x00de, 0x0040, 0x0040, 0x0040, 0x01f7, 0x015d, 0x0142, 0x01fa, 0x0274, 0x05bd, 0x05c1, 0x0303, 0x02b3, 0x02b3, 0x02b3, 0x0040, 0x0142, 0x0040, 0x0040, 0x0040, 0x0040, 0x028f, 0x00cf, 0x0150, 0x02af, 0x05c5, 0x00dd, 0x00de, 0x00de, 0x0030, 0x0030, 0x021d, 0x00de, 0x05c9, 0x05c9, 0x05c9, 0x05cc, 0x00de, 0x00de, 0x00de, 0x00de, 0x0040, 0x0040, 0x00ec, 0x01d6, 0x00cf, 0x0274, 0x01a1, 0x00de, 0x0030, 0x0030, 0x021d, 0x00de, 0x0030, 0x0030, 0x0030, 0x0030, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x0249, 0x014b, 0x0274, 0x00cf, 0x00de, 0x0030, 0x0030, 0x021d, 0x0567, 0x0040, 0x0040, 0x0040, 0x028f, 0x00cf, 0x00cf, 0x02ba, 0x00de, 0x0030, 0x0030, 0x0129, 0x0040, 0x00e2, 0x00de, 0x00de, 0x00df, 0x00de, 0x00de, 0x00de, 0x00de, 0x01fa, 0x01d8, 0x05d0, 0x05d3, 0x05d7, 0x0567, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x01f9, 0x00cf, 0x014b, 0x01fa, 0x02c3, 0x0299, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x0140, 0x00cf, 0x0215, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00ec, 0x00cf, 0x05da, 0x05dd, 0x0303, 0x05e1, 0x00de, 0x00de, 0x0140, 0x0150, 0x015e, 0x0040, 0x05e5, 0x05e7, 0x00cf, 0x00cf, 0x0150, 0x04d0, 0x05c7, 0x05eb, 0x00de, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x05c9, 0x05c9, 0x05cb, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x01a1, 0x00de, 0x00de, 0x00de, 0x0030, 0x0030, 0x021d, 0x00de, 0x0040, 0x0040, 0x00e4, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x01f7, 0x00cf, 0x012f, 0x00cf, 0x0274, 0x0303, 0x05ec, 0x00de, 0x00de, 0x0030, 0x0030, 0x0129, 0x0040, 0x0040, 0x0040, 0x00dd, 0x05f0, 0x0040, 0x0040, 0x0040, 0x0040, 0x014b, 0x00cf, 0x00cf, 0x00cf, 0x05f4, 0x00cf, 0x024a, 0x01a8, 0x00de, 0x00de, 0x0040, 0x00e2, 0x00e3, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0140, 0x012f, 0x017e, 0x0130, 0x00cf, 0x05f6, 0x00de, 0x00de, 0x0030, 0x0030, 0x021d, 0x00de, 0x0040, 0x00e3, 0x00e4, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x01f8, 0x01fb, 0x05f9, 0x02af, 0x00dd, 0x00de, 0x0030, 0x0030, 0x021d, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x05fd, 0x04e9, 0x0437, 0x00de, 0x0600, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x015d, 0x012f, 0x01d3, 0x0275, 0x02a2, 0x02a3, 0x02a3, 0x00de, 0x00de, 0x017e, 0x00de, 0x00de, 0x00de, 0x00de, 0x00dd, 0x00de, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0107, 0x04fd, 0x0040, 0x0040, 0x0040, 0x01a1, 0x00de, 0x00de, 0x029a, 0x0040, 0x0040, 0x0040, 0x00e2, 0x02b3, 0x0437, 0x00de, 0x00de, 0x0040, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0604, 0x0607, 0x0609, 0x041f, 0x0354, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x060c, 0x0040, 0x0040, 0x0040, 0x0058, 0x00d3, 0x0610, 0x0614, 0x0618, 0x0118, 0x00ec, 0x00cf, 0x00cf, 0x00cf, 0x0142, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x041f, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e2, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x014b, 0x00cf, 0x00cf, 0x0160, 0x015e, 0x00de, 0x00de, 0x00de, 0x00de, 0x0030, 0x0030, 0x021d, 0x029b, 0x0040, 0x0040, 0x0040, 0x0040, 0x01a1, 0x00cf, 0x0581, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x00cf, 0x00fa, 0x0266, 0x0040, 0x061c, 0x00de, 0x00de, 0x0030, 0x0030, 0x0620, 0x0040, 0x00e3, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00de, 0x00e1, 0x0623, 0x0623, 0x0626, 0x0264, 0x0030, 0x0030, 0x021d, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0263, 0x057d, 0x00de, 0x0040, 0x0040, 0x00e2, 0x014a, 0x01f9, 0x01fa, 0x01fa, 0x01fa, 0x01fa, 0x01fa, 0x01fa, 0x01fa, 0x01fa, 0x00de, 0x014a, 0x0215, 0x0040, 0x0040, 0x0040, 0x062a, 0x062e, 0x00de, 0x00de, 0x0632, 0x00de, 0x00de, 0x00de, 0x03bd, 0x03bd, 0x03bd, 0x03bd, 0x03bd, 0x0636, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x0638, 0x0463, 0x0463, 0x04a6, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x03bd, 0x063a, 0x03bd, 0x0635, 0x0464, 0x00de, 0x00de, 0x00de, 0x063e, 0x00de, 0x00de, 0x00de, 0x00de, 0x0642, 0x0645, 0x00de, 0x00de, 0x04ab, 0x00de, 0x00de, 0x0463, 0x0463, 0x0463, 0x0463, 0x0040, 0x0040, 0x00e2, 0x00de, 0x0040, 0x0040, 0x0040, 0x00dd, 0x00de, 0x0040, 0x0040, 0x01a1, 0x04cf, 0x00cf, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x0030, 0x0030, 0x021d, 0x00de, 0x00cf, 0x00cf, 0x00cf, 0x0142, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x012f, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e2, 0x00e1, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x015c, 0x00ef, 0x01f9, 0x028f, 0x00cf, 0x00cf, 0x00cf, 0x0215, 0x0140, 0x00cf, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00ed, 0x00ef, 0x0040, 0x0040, 0x0040, 0x0040, 0x00ed, 0x0133, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x03bd, 0x03bd, 0x03bd, 0x03bd, 0x03bd, 0x063b, 0x00de, 0x00de, 0x03bd, 0x03bd, 0x03bd, 0x03bd, 0x03bd, 0x0649, 0x00dd, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e4, 0x0144, 0x01a0, 0x00e1, 0x00e4, 0x0040, 0x0040, 0x00e3, 0x00e1, 0x0040, 0x00e1, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e3, 0x00e2, 0x00e1, 0x0040, 0x00e4, 0x0040, 0x00e4, 0x0040, 0x01ae, 0x00d8, 0x0040, 0x00e4, 0x0040, 0x0040, 0x0040, 0x01a1, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x018d, 0x0030, 0x0030, 0x0030, 0x0030, 0x0030, 0x0030, 0x0030, 0x0030, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x0215, 0x00ec, 0x00cf, 0x00cf, 0x00cf, 0x0118, 0x0040, 0x0117, 0x0040, 0x0040, 0x064d, 0x0451, 0x00de, 0x00de, 0x00de, 0x014a, 0x00cf, 0x00f9, 0x00cf, 0x00cf, 0x00cf, 0x00de, 0x00de, 0x00de, 0x00de, 0x00e1, 0x00e2, 0x00de, 0x00de, 0x00de, 0x00de, 0x00de, 0x00cf, 0x012f, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x01aa, 0x00cf, 0x0130, 0x019b, 0x012f, 0x00de, 0x0040, 0x0040, 0x0040, 0x0040, 0x01a1, 0x00de, 0x00de, 0x00de, 0x00de, 0x014a, 0x00de, 0x00de, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x00dd, 0x00cf, 0x0215, 0x0040, 0x01a1, 0x0030, 0x0030, 0x021d, 0x00d8, 0x00de, 0x00de, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x0199, 0x00de, 0x00de, 0x00de, 0x00de, 0x0040, 0x0040, 0x0040, 0x00cf, 0x0030, 0x0030, 0x021d, 0x0211, 0x0040, 0x0040, 0x0040, 0x00cf, 0x0030, 0x0030, 0x021d, 0x00de, 0x0040, 0x0040, 0x0040, 0x00ed, 0x0651, 0x0030, 0x0293, 0x00df, 0x0040, 0x00e2, 0x0040, 0x01a0, 0x0040, 0x0040, 0x0040, 0x00e2, 0x0040, 0x0170, 0x0040, 0x0040, 0x00cf, 0x012f, 0x00de, 0x00de, 0x0040, 0x00cf, 0x0215, 0x00de, 0x0030, 0x0030, 0x021d, 0x0655, 0x00de, 0x00de, 0x00de, 0x00de, 0x00e1, 0x0040, 0x0040, 0x0040, 0x04fd, 0x04fd, 0x00dd, 0x00de, 0x00de, 0x00e1, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x01a0, 0x0170, 0x00e1, 0x0040, 0x00e2, 0x0040, 0x01af, 0x00de, 0x0144, 0x00df, 0x01af, 0x00e1, 0x01a0, 0x0170, 0x01af, 0x01af, 0x01a0, 0x0170, 0x00e2, 0x0040, 0x00e2, 0x0040, 0x00e1, 0x01ae, 0x0040, 0x0040, 0x00e3, 0x0040, 0x0040, 0x0040, 0x0040, 0x00de, 0x00e1, 0x00e1, 0x00e3, 0x0040, 0x0040, 0x0040, 0x0040, 0x00de, 0x038a, 0x0659, 0x038a, 0x038a, 0x038a, 0x038a, 0x038a, 0x038a, 0x038a, 0x038a, 0x065a, 0x038a, 0x038a, 0x038a, 0x038a, 0x00c7, 0x00c7, 0x065e, 0x0661, 0x00c7, 0x00c7, 0x00c7, 0x00c7, 0x0665, 0x00c7, 0x00c7, 0x00c7, 0x00c7, 0x0338, 0x03a3, 0x0669, 0x00c7, 0x00c7, 0x066b, 0x00c7, 0x00c7, 0x00c7, 0x066f, 0x0672, 0x0673, 0x0674, 0x00c7, 0x00c7, 0x00c7, 0x0677, 0x038a, 0x038a, 0x038a, 0x038a, 0x0679, 0x067b, 0x067b, 0x067b, 0x067b, 0x067b, 0x067b, 0x067f, 0x038a, 0x038a, 0x038a, 0x0463, 0x0463, 0x04b0, 0x0463, 0x0463, 0x0463, 0x04af, 0x0683, 0x0685, 0x0686, 0x038a, 0x0463, 0x0463, 0x0689, 0x038a, 0x03f3, 0x038a, 0x038a, 0x038a, 0x0685, 0x03f3, 0x038a, 0x038a, 0x038a, 0x038a, 0x038a, 0x038a, 0x0685, 0x0685, 0x0685, 0x0685, 0x0685, 0x0685, 0x0685, 0x0685, 0x0659, 0x038a, 0x038a, 0x068c, 0x0685, 0x068e, 0x0685, 0x0685, 0x0685, 0x0685, 0x0685, 0x0685, 0x0685, 0x068f, 0x0685, 0x0685, 0x0685, 0x0685, 0x0685, 0x038a, 0x038a, 0x0693, 0x0685, 0x0685, 0x0685, 0x0685, 0x0685, 0x0697, 0x0685, 0x0699, 0x0685, 0x0685, 0x068d, 0x065a, 0x0685, 0x038a, 0x038a, 0x038a, 0x0685, 0x0685, 0x0685, 0x0685, 0x0659, 0x0659, 0x069a, 0x069d, 0x0685, 0x0685, 0x0685, 0x0685, 0x0685, 0x0685, 0x0685, 0x068d, 0x068f, 0x0685, 0x0685, 0x0685, 0x0685, 0x0685, 0x0685, 0x0685, 0x06a1, 0x0699, 0x0685, 0x06a4, 0x0697, 0x0685, 0x0685, 0x0685, 0x0685, 0x0685, 0x0685, 0x0685, 0x036a, 0x03a7, 0x06a7, 0x0685, 0x0685, 0x0685, 0x06a4, 0x03a7, 0x03a7, 0x0699, 0x0685, 0x0685, 0x06a5, 0x03a7, 0x03a7, 0x06ab, 0x0040, 0x0340, 0x06af, 0x068d, 0x0685, 0x0685, 0x0685, 0x0685, 0x038a, 0x038a, 0x038a, 0x038a, 0x06b3, 0x038a, 0x038a, 0x038a, 0x038a, 0x038a, 0x03f2, 0x038a, 0x038a, 0x038a, 0x038a, 0x038a, 0x03a3, 0x03a3, 0x038a, 0x038a, 0x038a, 0x038a, 0x038a, 0x03a3, 0x06af, 0x0685, 0x0685, 0x0685, 0x0685, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x040f, 0x06b7, 0x0040, 0x0685, 0x03f3, 0x038a, 0x0659, 0x068d, 0x068c, 0x038a, 0x0685, 0x038a, 0x038a, 0x065a, 0x0659, 0x038a, 0x0685, 0x0685, 0x0659, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x038a, 0x038a, 0x038a, 0x0040, 0x0040, 0x0040, 0x0040, 0x0040, 0x0389, 0x038a, 0x038a, 0x0685, 0x0685, 0x0685, 0x038a, 0x0659, 0x038a, 0x038a, 0x038a, 0x0040, 0x0040, 0x0040, 0x06bb, 0x0040, 0x0040, 0x0040, 0x0040, 0x06bb, 0x06bb, 0x0040, 0x0040, 0x06bf, 0x06bb, 0x0040, 0x0040, 0x06bb, 0x06bb, 0x0040, 0x0040, 0x0040, 0x0040, 0x06bf, 0x03a3, 0x03a3, 0x03a3, 0x06bb, 0x06c3, 0x06bb, 0x06bb, 0x06bb, 0x06bb, 0x06bb, 0x06bb, 0x06bb, 0x06bb, 0x0040, 0x0040, 0x0040, 0x0685, 0x0685, 0x0685, 0x0685, 0x0685, 0x0685, 0x06c7, 0x0685, 0x06c8, 0x0685, 0x0685, 0x0685, 0x0685, 0x0685, 0x0685, 0x03a3, 0x03a3, 0x03a3, 0x03a3, 0x03a3, 0x03a3, 0x03a3, 0x03a3, 0x038a, 0x038a, 0x038a, 0x038a, 0x0685, 0x0685, 0x0685, 0x0659, 0x0685, 0x0685, 0x03f3, 0x065a, 0x0685, 0x0685, 0x0685, 0x0685, 0x068d, 0x038a, 0x03f1, 0x0685, 0x0685, 0x0685, 0x036a, 0x0685, 0x0685, 0x03f3, 0x038a, 0x0685, 0x0685, 0x0659, 0x038a, 0x0040, 0x0040, 0x0040, 0x0040, 0x00e2, 0x0040, 0x0040, 0x0040, 0x038a, 0x038a, 0x038a, 0x038a, 0x038a, 0x038a, 0x038a, 0x06cc, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x0463, 0x0468, 0x06d0, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x00cf, 0x00cf, 0x00cf, 0x00cf, 0x0000, 0x0000, 0x0000, 0x0000, 0x00c7, 0x00c7, 0x00c7, 0x00c7, 0x00c7, 0x00c7, 0x00c7, 0x06d4, ]; #[rustfmt::skip] const STAGE3: [u16; 1752] = [ 0x0803, 0x0803, 0x0803, 0x0803, 0x0803, 0x0803, 0x0803, 0x0803, 0x0803, 0x0963, 0x0802, 0x0803, 0x0803, 0x0801, 0x0803, 0x0803, 0x0803, 0x0803, 0x0803, 0x0803, 0x0803, 0x0803, 0x0803, 0x0803, 0x0803, 0x0803, 0x0803, 0x0803, 0x0803, 0x0803, 0x0803, 0x0803, 0x0900, 0x0ac0, 0x0c00, 0x0d80, 0x0d00, 0x0cc0, 0x0d80, 0x0c00, 0x0bc0, 0x0a80, 0x0d80, 0x0d00, 0x0c40, 0x09c0, 0x0c40, 0x0d40, 0x0c80, 0x0c80, 0x0c80, 0x0c80, 0x0c80, 0x0c80, 0x0c80, 0x0c80, 0x0c80, 0x0c80, 0x0c40, 0x0c40, 0x0d80, 0x0d80, 0x0d80, 0x0ac0, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0bc0, 0x0d00, 0x0a80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0d80, 0x0bc0, 0x0940, 0x0a00, 0x0d80, 0x0803, 0x08c0, 0x1bc0, 0x0cc0, 0x0d00, 0x1d00, 0x0d00, 0x0d80, 0x1800, 0x0d8e, 0x1800, 0x0c00, 0x0d80, 0x0944, 0x1d8e, 0x0d80, 0x1cc0, 0x1d00, 0x1800, 0x1800, 0x1980, 0x0d80, 0x1800, 0x1800, 0x1800, 0x0c00, 0x1800, 0x1800, 0x1800, 0x1bc0, 0x0d80, 0x0d80, 0x1d80, 0x0d80, 0x0d80, 0x0d80, 0x1800, 0x0d80, 0x0d80, 0x1d80, 0x1d80, 0x0d80, 0x0d80, 0x1d80, 0x1d80, 0x1d80, 0x0d80, 0x1d80, 0x1d80, 0x0d80, 0x1d80, 0x0d80, 0x1d80, 0x0d80, 0x0d80, 0x0d80, 0x1d80, 0x1d80, 0x1d80, 0x1d80, 0x0d80, 0x0d80, 0x1800, 0x0980, 0x1800, 0x1800, 0x1800, 0x0980, 0x1800, 0x0d80, 0x0d80, 0x0d80, 0x1800, 0x1800, 0x1800, 0x1800, 0x0d80, 0x1800, 0x0d80, 0x1980, 0x0004, 0x0004, 0x0004, 0x0004, 0x00c4, 0x00c4, 0x00c4, 0x00c4, 0x0004, 0x0800, 0x0800, 0x0d80, 0x0d80, 0x0c40, 0x0d80, 0x0800, 0x0800, 0x0800, 0x0800, 0x0d80, 0x0d80, 0x0d80, 0x0800, 0x0d80, 0x0d80, 0x1d80, 0x1d80, 0x0800, 0x1d80, 0x0d80, 0x0d80, 0x0d80, 0x0004, 0x0004, 0x0d80, 0x0d80, 0x0c40, 0x0940, 0x0800, 0x0d80, 0x0d80, 0x0d00, 0x0800, 0x0004, 0x0004, 0x0004, 0x0940, 0x0004, 0x0004, 0x0ac0, 0x0004, 0x0486, 0x0486, 0x0486, 0x0486, 0x0d80, 0x0d80, 0x0cc0, 0x0cc0, 0x0cc0, 0x0004, 0x0004, 0x0004, 0x0ac0, 0x0ac0, 0x0ac0, 0x0c80, 0x0c80, 0x0cc0, 0x0c80, 0x0d80, 0x0d80, 0x0d80, 0x0004, 0x0d80, 0x0d80, 0x0d80, 0x0ac0, 0x0d80, 0x0004, 0x0004, 0x0486, 0x0d80, 0x0004, 0x0d80, 0x0d80, 0x0004, 0x0d80, 0x0004, 0x0004, 0x0c80, 0x0c80, 0x0d80, 0x0d80, 0x0800, 0x0586, 0x0004, 0x0004, 0x0004, 0x0800, 0x0004, 0x0d80, 0x0800, 0x0800, 0x0c40, 0x0ac0, 0x0d80, 0x0800, 0x0004, 0x0d00, 0x0d00, 0x0004, 0x0004, 0x0d80, 0x0004, 0x0004, 0x0004, 0x0800, 0x0800, 0x0d80, 0x0800, 0x0486, 0x0486, 0x0800, 0x0800, 0x0800, 0x0004, 0x0004, 0x0486, 0x0004, 0x0004, 0x0004, 0x0804, 0x0d80, 0x0d8d, 0x0d8d, 0x0d8d, 0x0d8d, 0x0004, 0x0804, 0x0004, 0x0d80, 0x0804, 0x0804, 0x0004, 0x0004, 0x0004, 0x0804, 0x0804, 0x0804, 0x000c, 0x0804, 0x0804, 0x0940, 0x0940, 0x0c80, 0x0c80, 0x0d80, 0x0004, 0x0804, 0x0804, 0x0d80, 0x0800, 0x0800, 0x0d80, 0x0d8d, 0x0800, 0x0d8d, 0x0d8d, 0x0800, 0x0d8d, 0x0800, 0x0800, 0x0d8d, 0x0d8d, 0x0800, 0x0800, 0x0004, 0x0800, 0x0800, 0x0804, 0x0800, 0x0800, 0x0804, 0x000c, 0x0d80, 0x0800, 0x0800, 0x0800, 0x0804, 0x0800, 0x0800, 0x0c80, 0x0c80, 0x0d8d, 0x0d8d, 0x0cc0, 0x0cc0, 0x0d80, 0x0cc0, 0x0d80, 0x0d00, 0x0d80, 0x0d80, 0x0004, 0x0800, 0x0004, 0x0004, 0x0804, 0x0800, 0x0d80, 0x0d80, 0x0800, 0x0800, 0x0004, 0x0800, 0x0804, 0x0804, 0x0004, 0x0004, 0x0800, 0x0800, 0x0004, 0x0d80, 0x0800, 0x0d80, 0x0800, 0x0d80, 0x0004, 0x0d80, 0x0800, 0x0d8d, 0x0d8d, 0x0d8d, 0x0004, 0x0804, 0x0800, 0x0804, 0x000c, 0x0800, 0x0800, 0x0d80, 0x0d00, 0x0800, 0x0800, 0x0d8d, 0x0004, 0x0004, 0x0800, 0x0004, 0x0804, 0x0804, 0x0004, 0x0d80, 0x0804, 0x0004, 0x0d80, 0x0d8d, 0x0d80, 0x0d80, 0x0800, 0x0800, 0x0804, 0x0804, 0x0004, 0x0804, 0x0804, 0x0800, 0x0804, 0x0804, 0x0004, 0x0800, 0x0800, 0x0d80, 0x0d00, 0x0d80, 0x0800, 0x0804, 0x0800, 0x0004, 0x0004, 0x000c, 0x0800, 0x0800, 0x0004, 0x0004, 0x0800, 0x0d8d, 0x0d8d, 0x0d8d, 0x0800, 0x0d80, 0x0800, 0x0800, 0x0800, 0x0980, 0x0d80, 0x0d80, 0x0d80, 0x0804, 0x0804, 0x0804, 0x0804, 0x0800, 0x0004, 0x0804, 0x0800, 0x0804, 0x0804, 0x0800, 0x0d80, 0x0d80, 0x0804, 0x000c, 0x0d86, 0x0d80, 0x0cc0, 0x0d80, 0x0d80, 0x0004, 0x0800, 0x0004, 0x0800, 0x0800, 0x0800, 0x0d00, 0x0004, 0x0004, 0x0004, 0x0d80, 0x0c80, 0x0c80, 0x0940, 0x0940, 0x0c80, 0x0c80, 0x0800, 0x0800, 0x0d80, 0x0980, 0x0980, 0x0980, 0x0d80, 0x0980, 0x0980, 0x08c0, 0x0980, 0x0980, 0x0940, 0x08c0, 0x0ac0, 0x0ac0, 0x0ac0, 0x08c0, 0x0d80, 0x0940, 0x0004, 0x0d80, 0x0004, 0x0bc0, 0x0a00, 0x0804, 0x0804, 0x0004, 0x0004, 0x0004, 0x0944, 0x0004, 0x0800, 0x0940, 0x0940, 0x0980, 0x0980, 0x0940, 0x0980, 0x0d80, 0x08c0, 0x08c0, 0x0800, 0x0004, 0x0804, 0x0004, 0x0004, 0x1007, 0x1007, 0x1007, 0x1007, 0x0808, 0x0808, 0x0808, 0x0808, 0x0809, 0x0809, 0x0809, 0x0809, 0x0d80, 0x0940, 0x0d80, 0x0d80, 0x0d80, 0x0a00, 0x0800, 0x0800, 0x0800, 0x0d80, 0x0d80, 0x0d80, 0x0940, 0x0940, 0x0d80, 0x0d80, 0x0004, 0x0804, 0x0800, 0x0800, 0x0804, 0x0940, 0x0940, 0x0800, 0x0d80, 0x0800, 0x0004, 0x0004, 0x0804, 0x0004, 0x0940, 0x0940, 0x0b40, 0x0800, 0x0940, 0x0d80, 0x0940, 0x0d00, 0x0d80, 0x0d80, 0x0ac0, 0x0ac0, 0x0940, 0x0940, 0x0980, 0x0d80, 0x0ac0, 0x0ac0, 0x0d80, 0x0004, 0x0004, 0x00c4, 0x0004, 0x0804, 0x0804, 0x0804, 0x0004, 0x0c80, 0x0c80, 0x0c80, 0x0800, 0x0804, 0x0004, 0x0804, 0x0800, 0x0800, 0x0800, 0x0940, 0x0940, 0x0dc0, 0x0940, 0x0940, 0x0940, 0x0dc0, 0x0dc0, 0x0dc0, 0x0dc0, 0x0004, 0x0d80, 0x0804, 0x0004, 0x0004, 0x0800, 0x0800, 0x0004, 0x0804, 0x0004, 0x0804, 0x0004, 0x0940, 0x0940, 0x0940, 0x0940, 0x0004, 0x0d80, 0x0d80, 0x0804, 0x0004, 0x0004, 0x0d80, 0x0800, 0x0004, 0x00c4, 0x0004, 0x0004, 0x0004, 0x0d80, 0x0980, 0x0d80, 0x0800, 0x0940, 0x0940, 0x0940, 0x08c0, 0x0940, 0x0940, 0x0940, 0x0084, 0x0004, 0x000f, 0x0004, 0x0004, 0x1940, 0x08c0, 0x0940, 0x1940, 0x1c00, 0x1c00, 0x0bc0, 0x0c00, 0x1800, 0x1800, 0x1d80, 0x0d80, 0x1b00, 0x1b00, 0x1b00, 0x1940, 0x0803, 0x0803, 0x0004, 0x0004, 0x0004, 0x08c0, 0x1cc0, 0x0cc0, 0x1cc0, 0x1cc0, 0x0cc0, 0x1cc0, 0x0cc0, 0x0cc0, 0x0d80, 0x0c00, 0x0c00, 0x1800, 0x0b4e, 0x0b40, 0x1d80, 0x0d80, 0x0c40, 0x0bc0, 0x0a00, 0x0b40, 0x0b4e, 0x0d80, 0x0d80, 0x0940, 0x0cc0, 0x0d80, 0x0940, 0x0940, 0x0940, 0x0044, 0x0584, 0x0584, 0x0584, 0x0803, 0x0004, 0x0004, 0x0d80, 0x0bc0, 0x0a00, 0x1800, 0x0d80, 0x0bc0, 0x0a00, 0x0800, 0x0d00, 0x0d00, 0x0d00, 0x0d00, 0x0cc0, 0x1d00, 0x0d00, 0x0d00, 0x0d00, 0x0cc0, 0x0d00, 0x0d00, 0x0d00, 0x0d80, 0x0d80, 0x0d80, 0x1cc0, 0x0d80, 0x0d80, 0x1d00, 0x0d80, 0x1800, 0x180e, 0x0d80, 0x0d8e, 0x0d80, 0x0d80, 0x0800, 0x0800, 0x0800, 0x1800, 0x0800, 0x0800, 0x0800, 0x1800, 0x1800, 0x0d80, 0x0d80, 0x180e, 0x180e, 0x180e, 0x180e, 0x0d80, 0x0d80, 0x0d8e, 0x0d8e, 0x0d80, 0x1800, 0x0d80, 0x1800, 0x1800, 0x0d80, 0x0d80, 0x1800, 0x0d00, 0x0d00, 0x0d80, 0x0d80, 0x0d80, 0x0b00, 0x0bc0, 0x0a00, 0x0bc0, 0x0a00, 0x0d80, 0x0d80, 0x15ce, 0x15ce, 0x0d8e, 0x1380, 0x1200, 0x0d80, 0x0d8e, 0x0d80, 0x0d80, 0x0d80, 0x0d8e, 0x0d80, 0x158e, 0x158e, 0x158e, 0x0d8e, 0x0d8e, 0x0d8e, 0x15ce, 0x0dce, 0x0dce, 0x15ce, 0x0d8e, 0x0d8e, 0x0d8e, 0x0d80, 0x1800, 0x1800, 0x180e, 0x1800, 0x1800, 0x0800, 0x1800, 0x1800, 0x1800, 0x1d80, 0x1800, 0x1800, 0x0d8e, 0x0d8e, 0x0d80, 0x0d80, 0x180e, 0x1800, 0x0d80, 0x0d80, 0x0d8e, 0x158e, 0x158e, 0x0d80, 0x0dce, 0x0dce, 0x0dce, 0x0dce, 0x0d8e, 0x180e, 0x1800, 0x0d8e, 0x180e, 0x0d8e, 0x0d8e, 0x180e, 0x180e, 0x15ce, 0x15ce, 0x080e, 0x080e, 0x0dce, 0x0d8e, 0x0dce, 0x0dce, 0x1dce, 0x0dce, 0x1dce, 0x0dce, 0x0d8e, 0x0d8e, 0x0d8e, 0x0d8e, 0x158e, 0x158e, 0x158e, 0x158e, 0x0d8e, 0x0dce, 0x0dce, 0x0dce, 0x180e, 0x0d8e, 0x180e, 0x0d8e, 0x180e, 0x180e, 0x0d8e, 0x180e, 0x1dce, 0x180e, 0x180e, 0x0d8e, 0x0d80, 0x0d80, 0x1580, 0x1580, 0x1580, 0x1580, 0x0d8e, 0x158e, 0x0d8e, 0x0d8e, 0x15ce, 0x15ce, 0x1dce, 0x1dce, 0x180e, 0x180e, 0x180e, 0x1dce, 0x158e, 0x1dce, 0x1dce, 0x180e, 0x1dce, 0x15ce, 0x180e, 0x180e, 0x180e, 0x1dce, 0x180e, 0x180e, 0x1dce, 0x1dce, 0x0d8e, 0x180e, 0x180e, 0x15ce, 0x180e, 0x1dce, 0x15ce, 0x15ce, 0x1dce, 0x15ce, 0x180e, 0x1dce, 0x1dce, 0x15ce, 0x180e, 0x15ce, 0x1dce, 0x1dce, 0x0dce, 0x158e, 0x0d80, 0x0d80, 0x0dce, 0x0dce, 0x15ce, 0x15ce, 0x0dce, 0x0dce, 0x0d8e, 0x0d8e, 0x0d80, 0x0d8e, 0x0d80, 0x158e, 0x0d80, 0x0d80, 0x0d80, 0x0d8e, 0x0d80, 0x0d80, 0x0d8e, 0x158e, 0x0d80, 0x158e, 0x0d80, 0x0d80, 0x0d80, 0x158e, 0x158e, 0x0d80, 0x100e, 0x0d80, 0x0d80, 0x0d80, 0x0c00, 0x0c00, 0x0c00, 0x0c00, 0x0d80, 0x0ac0, 0x0ace, 0x0bc0, 0x0a00, 0x1800, 0x1800, 0x0d80, 0x0bc0, 0x0a00, 0x0d80, 0x0d80, 0x0bc0, 0x0a00, 0x0bc0, 0x0a00, 0x0bc0, 0x0a00, 0x0d80, 0x0d80, 0x0d80, 0x0d8e, 0x0d8e, 0x0d8e, 0x0d80, 0x100e, 0x1800, 0x1800, 0x0800, 0x0ac0, 0x0940, 0x0940, 0x0d80, 0x0ac0, 0x0940, 0x0800, 0x0800, 0x0800, 0x0c00, 0x0c00, 0x0940, 0x0940, 0x0d80, 0x0940, 0x0bc0, 0x0940, 0x0d80, 0x0d80, 0x0c00, 0x0c00, 0x0d80, 0x0d80, 0x0c00, 0x0c00, 0x0bc0, 0x0a00, 0x0940, 0x0940, 0x0ac0, 0x0d80, 0x0940, 0x0940, 0x0940, 0x0d80, 0x0940, 0x0940, 0x0bc0, 0x0940, 0x0ac0, 0x0bc0, 0x0a80, 0x0bc0, 0x0a80, 0x0bc0, 0x0a80, 0x0940, 0x0800, 0x0800, 0x15c0, 0x15c0, 0x15c0, 0x15c0, 0x0800, 0x15c0, 0x15c0, 0x0800, 0x0800, 0x1140, 0x1200, 0x1200, 0x15c0, 0x1340, 0x15c0, 0x15c0, 0x1380, 0x1200, 0x1380, 0x1200, 0x15c0, 0x15c0, 0x1340, 0x1380, 0x1200, 0x1200, 0x15c0, 0x15c0, 0x0004, 0x0004, 0x1004, 0x1004, 0x15ce, 0x15c0, 0x15c0, 0x15c0, 0x1000, 0x15c0, 0x15c0, 0x15c0, 0x1340, 0x15ce, 0x15c0, 0x0dc0, 0x0800, 0x1000, 0x15c0, 0x1000, 0x15c0, 0x1000, 0x1000, 0x0800, 0x0004, 0x0004, 0x1340, 0x1340, 0x1340, 0x15c0, 0x1340, 0x1000, 0x15c0, 0x1000, 0x1000, 0x15c0, 0x1000, 0x1340, 0x1340, 0x15c0, 0x0800, 0x0800, 0x0800, 0x15c0, 0x1000, 0x1000, 0x1000, 0x1000, 0x15c0, 0x15c0, 0x15c0, 0x15ce, 0x15c0, 0x15c0, 0x0d80, 0x0940, 0x0ac0, 0x0940, 0x0004, 0x0004, 0x0d80, 0x0940, 0x0804, 0x0004, 0x0004, 0x0804, 0x0cc0, 0x0d80, 0x0800, 0x0800, 0x0980, 0x0980, 0x0ac0, 0x0ac0, 0x0804, 0x0804, 0x0d80, 0x0d80, 0x0980, 0x0d80, 0x0d80, 0x0004, 0x0004, 0x0940, 0x0940, 0x1007, 0x0800, 0x0800, 0x0800, 0x0804, 0x0dc0, 0x0dc0, 0x0dc0, 0x0940, 0x0dc0, 0x0dc0, 0x0800, 0x0940, 0x0800, 0x0800, 0x0dc0, 0x0dc0, 0x0d80, 0x0804, 0x0004, 0x0800, 0x0004, 0x0804, 0x0804, 0x0940, 0x100a, 0x100b, 0x100b, 0x100b, 0x100b, 0x0808, 0x0808, 0x0808, 0x0800, 0x0800, 0x0800, 0x0809, 0x0d80, 0x0d80, 0x0a00, 0x0bc0, 0x0cc0, 0x0d80, 0x0d80, 0x0d80, 0x0004, 0x0004, 0x0004, 0x1004, 0x1200, 0x1200, 0x1200, 0x1340, 0x12c0, 0x12c0, 0x1380, 0x1200, 0x1300, 0x0800, 0x0800, 0x00c4, 0x0004, 0x00c4, 0x0004, 0x00c4, 0x00c4, 0x0004, 0x1200, 0x1380, 0x1200, 0x1380, 0x1200, 0x15c0, 0x15c0, 0x1380, 0x1200, 0x15c0, 0x15c0, 0x15c0, 0x1200, 0x15c0, 0x1200, 0x0800, 0x1340, 0x1340, 0x12c0, 0x12c0, 0x15c0, 0x1500, 0x14c0, 0x15c0, 0x0d80, 0x0800, 0x0800, 0x0044, 0x0800, 0x12c0, 0x15c0, 0x15c0, 0x1500, 0x14c0, 0x15c0, 0x15c0, 0x1200, 0x15c0, 0x1200, 0x15c0, 0x15c0, 0x1340, 0x1340, 0x15c0, 0x15c0, 0x15c0, 0x12c0, 0x15c0, 0x15c0, 0x15c0, 0x1380, 0x15c0, 0x1200, 0x15c0, 0x1380, 0x1200, 0x0a00, 0x0b80, 0x0a00, 0x0b40, 0x0dc0, 0x0800, 0x0dc0, 0x0dc0, 0x0dc0, 0x0b44, 0x0b44, 0x0dc0, 0x0dc0, 0x0dc0, 0x0800, 0x0800, 0x0800, 0x14c0, 0x1500, 0x15c0, 0x15c0, 0x1500, 0x1500, 0x0800, 0x0940, 0x0940, 0x0940, 0x0800, 0x0d80, 0x0004, 0x0800, 0x0800, 0x0d80, 0x0d80, 0x0800, 0x0940, 0x0d80, 0x0004, 0x0004, 0x0800, 0x0940, 0x0940, 0x0b00, 0x0800, 0x0004, 0x0004, 0x0940, 0x0d80, 0x0d80, 0x0800, 0x0004, 0x0940, 0x0800, 0x0800, 0x0800, 0x00c4, 0x0d80, 0x0486, 0x0940, 0x0940, 0x0004, 0x0800, 0x0486, 0x0800, 0x0800, 0x0004, 0x0800, 0x0c80, 0x0c80, 0x0d80, 0x0804, 0x0804, 0x0d80, 0x0d86, 0x0d86, 0x0940, 0x0004, 0x0004, 0x0004, 0x0c80, 0x0c80, 0x0d80, 0x0980, 0x0940, 0x0d80, 0x0004, 0x0d80, 0x0940, 0x0800, 0x0800, 0x0004, 0x0940, 0x0804, 0x0804, 0x0800, 0x0800, 0x0800, 0x0dc0, 0x0004, 0x0800, 0x0804, 0x0800, 0x0804, 0x0004, 0x0806, 0x0004, 0x0dc0, 0x0dc0, 0x0800, 0x0dc0, 0x0004, 0x0980, 0x0940, 0x0940, 0x0ac0, 0x0ac0, 0x0d80, 0x0d80, 0x0004, 0x0940, 0x0940, 0x0d80, 0x0980, 0x0980, 0x0980, 0x0980, 0x0800, 0x0800, 0x0800, 0x0804, 0x0800, 0x0800, 0x0004, 0x0804, 0x0004, 0x0806, 0x0804, 0x0806, 0x0804, 0x0004, 0x0804, 0x0d86, 0x0004, 0x0004, 0x0004, 0x0980, 0x0940, 0x0980, 0x0d80, 0x0004, 0x0d86, 0x0d86, 0x0d86, 0x0d86, 0x0004, 0x0004, 0x0980, 0x0940, 0x0940, 0x0800, 0x0800, 0x0980, 0x0ac0, 0x0d80, 0x0d80, 0x0800, 0x0804, 0x0004, 0x0004, 0x0d86, 0x0004, 0x0004, 0x0800, 0x0804, 0x0800, 0x0800, 0x0940, 0x0004, 0x0004, 0x0806, 0x0804, 0x0bc0, 0x0bc0, 0x0bc0, 0x0a00, 0x0a00, 0x0d80, 0x0d80, 0x0a00, 0x0d80, 0x0bc0, 0x0a00, 0x0a00, 0x00c4, 0x00c4, 0x00c4, 0x03c4, 0x0204, 0x00c4, 0x00c4, 0x00c4, 0x03c4, 0x0204, 0x03c4, 0x0204, 0x0940, 0x0d80, 0x0800, 0x0800, 0x0c80, 0x0c80, 0x0800, 0x0d80, 0x0d80, 0x0d80, 0x0d88, 0x0d88, 0x0d88, 0x0d80, 0x1340, 0x1340, 0x1340, 0x1340, 0x00c4, 0x0800, 0x0800, 0x0800, 0x1004, 0x1004, 0x0800, 0x0800, 0x1580, 0x1580, 0x0800, 0x0800, 0x0800, 0x1580, 0x1580, 0x1580, 0x0800, 0x0800, 0x1000, 0x0800, 0x1000, 0x1000, 0x1000, 0x0800, 0x1000, 0x0800, 0x0800, 0x1580, 0x1580, 0x1580, 0x0d80, 0x0004, 0x0d80, 0x0d80, 0x0940, 0x0d80, 0x0c80, 0x0c80, 0x0c80, 0x0800, 0x0800, 0x0bc0, 0x0bc0, 0x15ce, 0x0dce, 0x0dce, 0x0dce, 0x15ce, 0x1800, 0x1800, 0x1800, 0x0800, 0x0d8e, 0x0d8e, 0x0d8e, 0x1800, 0x1800, 0x0d80, 0x0d8e, 0x180e, 0x180e, 0x1800, 0x1800, 0x180e, 0x180e, 0x1800, 0x1800, 0x100e, 0x1800, 0x100e, 0x100e, 0x100e, 0x100e, 0x1800, 0x0d8e, 0x0dce, 0x0dce, 0x0805, 0x0805, 0x0805, 0x0805, 0x15c0, 0x15ce, 0x15ce, 0x0dce, 0x15c0, 0x15c0, 0x15ce, 0x15ce, 0x15ce, 0x15ce, 0x15c0, 0x0dce, 0x0dce, 0x0dce, 0x15ce, 0x15ce, 0x15ce, 0x0dce, 0x15ce, 0x15ce, 0x0d8e, 0x0d8e, 0x0dce, 0x0dce, 0x15ce, 0x158e, 0x158e, 0x15ce, 0x15ce, 0x15ce, 0x15c4, 0x15c4, 0x15c4, 0x15c4, 0x158e, 0x15ce, 0x158e, 0x15ce, 0x15ce, 0x15ce, 0x158e, 0x158e, 0x158e, 0x15ce, 0x158e, 0x158e, 0x0d80, 0x0d80, 0x0d8e, 0x0d8e, 0x0dce, 0x15ce, 0x0dce, 0x0dce, 0x15ce, 0x0dce, 0x0c00, 0x0b40, 0x0b40, 0x0b40, 0x080e, 0x080e, 0x080e, 0x080e, 0x0d80, 0x0d80, 0x080e, 0x080e, 0x0d8e, 0x0d8e, 0x080e, 0x080e, 0x15ce, 0x15ce, 0x15ce, 0x0dc0, 0x15ce, 0x0dce, 0x0dce, 0x0800, 0x0800, 0x0803, 0x0004, 0x0803, 0x0803, 0x1800, 0x1800, 0x0800, 0x0800, ]; #[rustfmt::skip] const GRAPHEME_JOIN_RULES: [[u32; 16]; 2] = [ [ 0b00111100111111111111110011111111, 0b11111111111111111111111111001111, 0b11111111111111111111111111111111, 0b11111111111111111111111111111111, 0b00111100111111111111110011111111, 0b00111100111111111111010011111111, 0b00000000000000000000000011111100, 0b00111100000011000011110011111111, 0b00111100111100001111110011111111, 0b00111100111100111111110011111111, 0b00111100111100001111110011111111, 0b00111100111100111111110011111111, 0b00110000111111111111110011111111, 0b00111100111111111111110011111111, 0b00111100111111111111110011111111, 0b00001100111111111111110011111111, ], [ 0b00111100111111111111110011111111, 0b11111111111111111111111111001111, 0b11111111111111111111111111111111, 0b11111111111111111111111111111111, 0b00111100111111111111110011111111, 0b00111100111111111111110011111111, 0b00000000000000000000000011111100, 0b00111100000011000011110011111111, 0b00111100111100001111110011111111, 0b00111100111100111111110011111111, 0b00111100111100001111110011111111, 0b00111100111100111111110011111111, 0b00110000111111111111110011111111, 0b00111100111111111111110011111111, 0b00111100111111111111110011111111, 0b00001100111111111111110011111111, ], ]; #[rustfmt::skip] const LINE_BREAK_JOIN_RULES: [u32; 25] = [ 0b00000000001000110011111110111010, 0b00000000111111111111111111111111, 0b00000000000000000000000000010000, 0b00000000111111111111111111111111, 0b00000000001000110000111100010010, 0b00000000001000110011111110110010, 0b00000000111111111111111111111111, 0b00000000001001110011111100110010, 0b00000000001110110011111110111010, 0b00000000001110110011111110111010, 0b00000000011111110011111110111010, 0b00000000001000110011111110111010, 0b00000000001000110011111110111010, 0b00000000001000110011111110111010, 0b00000000111111111111111111111111, 0b00000000111111111111111111111111, 0b00000000111111111111111111111111, 0b00000000011111110011111110111010, 0b00000000011111111011111110111010, 0b00000000011001111111111110111010, 0b00000000111001111111111110111010, 0b00000000001111110011111110111010, 0b00000000011111111011111110111010, 0b00000000001010110011111110111010, 0b00000000000000000000000000000000, ]; #[inline(always)] pub fn ucd_grapheme_cluster_lookup(cp: char) -> usize { unsafe { let cp = cp as usize; if cp < 0x80 { return STAGE3[cp] as usize; } let s = *STAGE0.get_unchecked(cp >> 11) as usize; let s = *STAGE1.get_unchecked(s + ((cp >> 5) & 63)) as usize; let s = *STAGE2.get_unchecked(s + ((cp >> 2) & 7)) as usize; *STAGE3.get_unchecked(s + (cp & 3)) as usize } } #[inline(always)] pub fn ucd_grapheme_cluster_joins(state: u32, lead: usize, trail: usize) -> u32 { unsafe { let l = lead & 31; let t = trail & 31; let s = GRAPHEME_JOIN_RULES.get_unchecked(state as usize); (s[l] >> (t * 2)) & 3 } } #[inline(always)] pub fn ucd_grapheme_cluster_joins_done(state: u32) -> bool { state == 3 } #[inline(always)] pub fn ucd_grapheme_cluster_character_width(val: usize, ambiguous_width: usize) -> usize { let mut w = val >> 11; if w > 2 { cold_path(); w = ambiguous_width; } w } #[inline(always)] pub fn ucd_line_break_joins(lead: usize, trail: usize) -> bool { unsafe { let l = (lead >> 6) & 31; let t = (trail >> 6) & 31; let s = *LINE_BREAK_JOIN_RULES.get_unchecked(l); ((s >> t) & 1) != 0 } } #[inline(always)] pub fn ucd_start_of_text_properties() -> usize { 0x603 } #[inline(always)] pub fn ucd_tab_properties() -> usize { 0x963 } #[inline(always)] pub fn ucd_linefeed_properties() -> usize { 0x802 } #[cold] #[inline(always)] fn cold_path() {} // END: Generated by grapheme-table-gen ================================================ FILE: crates/edit/src/vt.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! Our VT parser. use std::time; use stdext::unicode::Utf8Chars; 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 ambiguity 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. // Realistically though, this could be much lower. // // However, there seems to be issues with OpenSSH on Windows. // See: https://github.com/PowerShell/Win32-OpenSSH/issues/2275 State::Esc => time::Duration::from_millis(100), _ => 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<'input> Stream<'_, '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 } /// Returns `true` if the input has been fully parsed. pub fn done(&self) -> bool { self.off >= self.input.len() } /// Decodes and consumes the next UTF-8 character from the input. pub fn next_char(&mut self) -> char { let mut iter = Utf8Chars::new(self.input.as_bytes(), self.off); let c = iter.next().unwrap_or('\0'); self.off = iter.offset(); c } /// Parses the next VT sequence from the previously given input. #[allow( clippy::should_implement_trait, reason = "can't implement Iterator because this is a lending iterator" )] pub fn next(&mut self) -> Option> { 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!(self.parser.state, State::Esc) { self.parser.state = State::Ground; return Some(Token::Esc('\0')); } while self.off < bytes.len() { // TODO: The state machine can be roughly broken up into two parts: // * Wants to parse 1 `char` at a time: Ground, Esc, Ss3 // These could all be unified to a single call to `decode_next()`. // * Wants to bulk-process bytes: Csi, Osc, Dcs // We should do that so the UTF8 handling is a bit more "unified". match self.parser.state { State::Ground => match bytes[self.off] { 0x1b => { self.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 => match self.next_char() { '[' => { self.parser.state = State::Csi; self.parser.csi.private_byte = '\0'; self.parser.csi.final_byte = '\0'; while self.parser.csi.param_count > 0 { self.parser.csi.param_count -= 1; self.parser.csi.params[self.parser.csi.param_count] = 0; } } ']' => { self.parser.state = State::Osc; } 'O' => { self.parser.state = State::Ss3; } 'P' => { self.parser.state = State::Dcs; } c => { self.parser.state = State::Ground; return Some(Token::Esc(c)); } }, State::Ss3 => { self.parser.state = State::Ground; return Some(Token::SS3(self.next_char())); } State::Csi => { loop { // If we still have slots left, parse the parameter. if self.parser.csi.param_count < self.parser.csi.params.len() { let dst = &mut self.parser.csi.params[self.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 => { self.parser.state = State::Ground; self.parser.csi.final_byte = c as char; if self.parser.csi.param_count != 0 || self.parser.csi.params[0] != 0 { self.parser.csi.param_count += 1; } return Some(Token::Csi(&self.parser.csi)); } b';' => self.parser.csi.param_count += 1, b'<'..=b'?' => self.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() { self.parser.state = match self.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 = self.parser.state; if !partial { self.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 = self.parser.state; // Consume the terminator (one byte in the previous input and this byte). self.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]`). self.parser.state = match self.parser.state { State::OscEsc => State::Osc, _ => State::Dcs, }; return match self.parser.state { State::Osc => Some(Token::Osc { data: "\x1b", partial: true }), _ => Some(Token::Dcs { data: "\x1b", partial: true }), }; } } } } None } } ================================================ FILE: crates/stdext/Cargo.toml ================================================ [package] name = "stdext" version = "0.0.0" edition.workspace = true license.workspace = true repository.workspace = true rust-version.workspace = true [features] single-threaded = [] [target.'cfg(unix)'.dependencies] libc = "0.2" ================================================ FILE: crates/stdext/src/alloc.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use std::alloc::{Layout, alloc, dealloc, handle_alloc_error, realloc}; use std::ptr::NonNull; pub trait Allocator { /// # Safety /// /// It's an allocator trait. It's unsafe. /// Note that `old_ptr` may be invalid if `old_size` is 0. unsafe fn realloc( &self, old_ptr: NonNull, old_size: usize, new_size: usize, align: usize, ) -> NonNull<[u8]>; /// # Safety /// /// Naturally, `ptr` must be valid. unsafe fn dealloc(&self, ptr: NonNull, size: usize, align: usize); } pub struct GlobalAllocator; impl Allocator for GlobalAllocator { unsafe fn realloc( &self, old_ptr: NonNull, old_size: usize, new_size: usize, align: usize, ) -> NonNull<[u8]> { unsafe { let new_ptr = if old_size == 0 { let layout = Layout::from_size_align_unchecked(new_size, align); alloc(layout) } else { let layout = Layout::from_size_align_unchecked(old_size, align); realloc(old_ptr.as_ptr(), layout, new_size) }; let Some(new_ptr) = NonNull::new(new_ptr) else { let layout = Layout::from_size_align_unchecked(new_size, align); handle_alloc_error(layout); }; NonNull::slice_from_raw_parts(new_ptr, new_size) } } unsafe fn dealloc(&self, ptr: NonNull, size: usize, align: usize) { unsafe { let layout = Layout::from_size_align_unchecked(size, align); dealloc(ptr.as_ptr(), layout); } } } ================================================ FILE: crates/stdext/src/arena/debug.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #![allow(clippy::missing_safety_doc, clippy::mut_from_ref)] use std::io; use std::ops::Deref; use std::ptr::NonNull; use super::release; use crate::alloc::Allocator; /// 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 stdext::arena::scratch_arena; /// /// let mut scratch1 = scratch_arena(None); /// let mut scratch2 = scratch_arena(None); /// /// let foo = scratch1.alloc_uninit::(); /// /// // 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 Self::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) -> io::Result { Ok(Self::Owned { arena: release::Arena::new(capacity)? }) } pub(super) fn delegated(delegate: &release::Arena) -> Self { let borrow = delegate.borrows.get() + 1; delegate.borrows.set(borrow); Self::Delegated { delegate: unsafe { &*(delegate as *const _) }, borrow } } #[inline] pub(super) fn delegate_target(&self) -> &release::Arena { match *self { Self::Delegated { delegate, borrow } => { assert!( borrow == delegate.borrows.get(), "Arena already borrowed by a newer ScratchArena" ); delegate } Self::Owned { ref arena } => arena, } } #[inline] pub(super) fn delegate_target_unchecked(&self) -> &release::Arena { match self { Self::Delegated { delegate, .. } => delegate, Self::Owned { arena } => arena, } } } impl Deref for Arena { type Target = release::Arena; #[inline] fn deref(&self) -> &Self::Target { self.delegate_target() } } impl Allocator for Arena { unsafe fn realloc( &self, old_ptr: NonNull, old_size: usize, new_size: usize, align: usize, ) -> NonNull<[u8]> { unsafe { self.delegate_target().realloc(old_ptr, old_size, new_size, align) } } unsafe fn dealloc(&self, _ptr: NonNull, _size: usize, _align: usize) {} } ================================================ FILE: crates/stdext/src/arena/fs.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use std::fs::File; use std::io::{self, Read}; use std::mem::MaybeUninit; use std::path::Path; use std::slice::from_raw_parts_mut; use crate::arena::Arena; use crate::collections::{BString, BVec}; pub fn read_to_vec>(arena: &'_ Arena, path: P) -> io::Result> { fn inner<'a>(arena: &'a Arena, path: &Path) -> io::Result> { let mut file = File::open(path)?; let mut vec = BVec::empty(); const MIN_SIZE: usize = 1024; const MAX_SIZE: usize = 128 * 1024; let mut buf_size = MIN_SIZE; loop { vec.reserve(arena, buf_size); let spare = vec.spare_capacity_mut(); let to_read = spare.len().min(buf_size); match file_read_uninit(&mut file, &mut spare[..to_read]) { Ok(0) => break, Ok(n) => { unsafe { vec.set_len(vec.len() + n) }; buf_size = (buf_size * 2).min(MAX_SIZE); } Err(e) if e.kind() == io::ErrorKind::Interrupted => {} Err(e) => return Err(e), } } Ok(vec) } inner(arena, path.as_ref()) } pub fn read_to_string>(arena: &Arena, path: P) -> io::Result> { fn inner<'a>(arena: &'a Arena, path: &Path) -> io::Result> { let vec = read_to_vec(arena, path)?; BString::from_utf8(vec).map_err(|_| { io::Error::new(io::ErrorKind::InvalidData, "stream did not contain valid UTF-8") }) } inner(arena, path.as_ref()) } fn file_read_uninit(file: &mut T, buf: &mut [MaybeUninit]) -> io::Result { unsafe { let buf_slice = from_raw_parts_mut(buf.as_mut_ptr() as *mut u8, buf.len()); let n = file.read(buf_slice)?; Ok(n) } } ================================================ FILE: crates/stdext/src/arena/mod.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! Arena allocators. Small and fast. #[cfg(debug_assertions)] mod debug; mod fs; mod release; mod scratch; #[cfg(all(not(doc), debug_assertions))] pub use self::debug::*; pub use self::fs::*; #[cfg(any(doc, not(debug_assertions)))] pub use self::release::*; pub use self::scratch::*; #[macro_export] macro_rules! arena_format { ($arena:expr, $($arg:tt)*) => {{ use std::fmt::Write as _; let mut output = ::stdext::collections::BString::empty(); let _ = output.formatter($arena).write_fmt(format_args!($($arg)*)); output }} } #[macro_export] macro_rules! arena_write_fmt { ($arena:expr, $output:expr, $($arg:tt)*) => {{ use std::fmt::Write as _; let _ = $output.formatter($arena).write_fmt(format_args!($($arg)*)); }} } ================================================ FILE: crates/stdext/src/arena/release.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #![allow(clippy::mut_from_ref)] use std::cell::Cell; use std::mem::MaybeUninit; use std::ptr::{self, NonNull}; use std::{io, mem, slice}; use crate::alloc::Allocator; use crate::sys; #[cfg(target_pointer_width = "32")] const ALLOC_CHUNK_SIZE: usize = 32 * 1024; #[cfg(target_pointer_width = "64")] const ALLOC_CHUNK_SIZE: usize = 64 * 1024; /// An arena allocator. /// /// 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 infamously 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. /// ///
/// /// **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, capacity: usize, commit: Cell, offset: Cell, /// See [`super::debug`], which uses this for borrow tracking. #[cfg(debug_assertions)] pub(super) borrows: Cell, } 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) -> io::Result { let capacity = (capacity.max(1) + ALLOC_CHUNK_SIZE - 1) & !(ALLOC_CHUNK_SIZE - 1); let base = unsafe { sys::virtual_reserve(capacity)? }; Ok(Self { base, capacity, commit: Cell::new(0), offset: Cell::new(0), #[cfg(debug_assertions)] borrows: Cell::new(0), }) } pub fn is_empty(&self) -> bool { self.base == NonNull::dangling() } 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) -> NonNull<[u8]> { 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); 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) -> NonNull<[u8]> { let offset = self.offset.get(); let commit_old = self.commit.get(); let commit_new = (end + ALLOC_CHUNK_SIZE - 1) & !(ALLOC_CHUNK_SIZE - 1); if commit_new > self.capacity || unsafe { sys::virtual_commit(self.base.add(commit_old), commit_new - commit_old).is_err() } { // Panicking inside this [cold] function has the benefit of removing duplicated panic code from any // inlined alloc() function. If we ever add fallible allocations, we should probably duplicate alloc_raw() // and alloc_raw_bump() instead of returning a Result here and calling unwrap() in the common path. panic!("out of memory"); } if cfg!(debug_assertions) { 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); unsafe { NonNull::slice_from_raw_parts(self.base.add(beg), end - beg) } } #[inline] #[allow(clippy::mut_from_ref)] pub fn alloc_uninit(&self) -> &mut MaybeUninit { let bytes = mem::size_of::(); let alignment = mem::align_of::(); let ptr = self.alloc_raw(bytes, alignment); unsafe { ptr.cast().as_mut() } } #[inline] #[allow(clippy::mut_from_ref)] pub fn alloc_uninit_array(&self) -> &mut [MaybeUninit; N] { let bytes = mem::size_of::<[MaybeUninit; N]>(); let alignment = mem::align_of::<[MaybeUninit; N]>(); let ptr = self.alloc_raw(bytes, alignment); unsafe { ptr.cast().as_mut() } } #[inline] #[allow(clippy::mut_from_ref)] pub fn alloc_uninit_slice(&self, count: usize) -> &mut [MaybeUninit] { let bytes = mem::size_of::() * count; let alignment = mem::align_of::(); let ptr = self.alloc_raw(bytes, alignment); unsafe { slice::from_raw_parts_mut(ptr.cast().as_ptr(), count) } } /// A workaround for `alloc_uninit_slice(count).write_filled()` being unstable (`maybe_uninit_fill`). #[inline] #[allow(clippy::mut_from_ref)] pub fn alloc_slice(&self, count: usize, value: T) -> &mut [T] { let slice = self.alloc_uninit_slice(count); slice.fill(MaybeUninit::new(value)); unsafe { slice.assume_init_mut() } } } impl Drop for Arena { fn drop(&mut self) { if !self.is_empty() { unsafe { sys::virtual_release(self.base, self.capacity) }; } } } impl Default for Arena { fn default() -> Self { Self::empty() } } impl Allocator for Arena { unsafe fn realloc( &self, old_ptr: NonNull, old_size: usize, new_size: usize, align: usize, ) -> NonNull<[u8]> { if unsafe { old_ptr.add(old_size) == self.base.add(self.offset.get()) } { // Check if it's the last allocation we made. // If so, we can grow/shrink it in place without copying. if new_size > old_size { self.alloc_raw(new_size - old_size, align); } else { self.offset.set(self.offset.get() - old_size + new_size); } NonNull::slice_from_raw_parts(old_ptr, new_size) } else if new_size > old_size { // Otherwise, we have to allocate a new area and copy it over. unsafe { let new_ptr = self.alloc_raw(new_size, align); ptr::copy_nonoverlapping(old_ptr.as_ptr(), new_ptr.as_ptr() as *mut _, old_size); new_ptr } } else { debug_assert!(false, "only the last allocation can be shrunk"); NonNull::slice_from_raw_parts(old_ptr, old_size) } } unsafe fn dealloc(&self, _ptr: NonNull, _size: usize, _align: usize) {} } ================================================ FILE: crates/stdext/src/arena/scratch.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use std::io; #[cfg(debug_assertions)] use std::marker::PhantomData; use std::ops::Deref; #[cfg(debug_assertions)] use super::debug; use super::{Arena, release}; use crate::helpers::*; /// Borrows an [`Arena`] for temporary allocations. /// /// See [`scratch_arena`]. #[cfg(debug_assertions)] pub struct ScratchArena<'a> { arena: debug::Arena, offset: usize, _phantom: 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: PhantomData, offset } } } #[cfg(not(debug_assertions))] impl<'a> ScratchArena<'a> { fn new(arena: &'a release::Arena) -> Self { let offset = arena.offset(); ScratchArena { arena, offset } } } impl Drop for ScratchArena<'_> { fn drop(&mut self) { unsafe { self.arena.reset(self.offset) }; } } #[cfg(debug_assertions)] impl Deref for ScratchArena<'_> { type Target = debug::Arena; fn deref(&self) -> &Self::Target { &self.arena } } #[cfg(not(debug_assertions))] impl Deref for ScratchArena<'_> { type Target = Arena; fn deref(&self) -> &Self::Target { self.arena } } mod single_threaded { use super::*; static mut S_SCRATCH: [release::Arena; 2] = const { [release::Arena::empty(), release::Arena::empty()] }; /// Initialize the scratch arenas with a given capacity. /// Call this before using [`scratch_arena`]. #[allow(dead_code)] pub fn init(capacity: usize) -> io::Result<()> { unsafe { for s in &mut S_SCRATCH[..] { *s = release::Arena::new(capacity)?; } } Ok(()) } /// Need an arena for temporary allocations? [`scratch_arena`] got you covered. /// Call [`scratch_arena`] and it'll return an [`Arena`] that resets when it goes out of scope. /// /// --- /// /// Most methods make just two kinds of allocations: /// * Interior: Temporary data that can be deallocated when the function returns. /// * Exterior: Data that is returned to the caller and must remain alive until the caller stops using it. /// /// Such methods only have two lifetimes, for which you consequently also only need two arenas. /// ...even if your method calls other methods recursively! This is because the exterior allocations /// of a callee are simply interior allocations to the caller, and so on, recursively. /// /// This works as long as the two arenas flip/flop between being used as interior/exterior allocator /// along the callstack. To ensure that is the case, we use a recursion counter in debug builds. /// /// This approach was described among others at: /// /// # Safety /// /// If your function takes an [`Arena`] argument, you **MUST** pass it to `scratch_arena` as `Some(&arena)`. #[allow(dead_code)] pub fn scratch_arena(conflict: Option<&Arena>) -> ScratchArena<'static> { unsafe { #[cfg(debug_assertions)] let conflict = conflict.map(|a| a.delegate_target_unchecked()); let index = opt_ptr_eq(conflict, Some(&S_SCRATCH[0])) as usize; let arena = &S_SCRATCH[index]; ScratchArena::new(arena) } } } mod multi_threaded { use std::cell::Cell; use std::ptr; use std::sync::atomic::{AtomicUsize, Ordering}; use super::*; thread_local! { static S_SCRATCH: [Cell; 2] = const { [Cell::new(release::Arena::empty()), Cell::new(release::Arena::empty())] }; } static INIT_SIZE: AtomicUsize = AtomicUsize::new(128 * MEBI); /// Sets the default scratch arena size. pub fn init(capacity: usize) -> io::Result<()> { if capacity != 0 { INIT_SIZE.store(capacity, Ordering::Relaxed); } Ok(()) } /// See `single_threaded::scratch_arena`. #[allow(dead_code)] pub fn scratch_arena(conflict: Option<&Arena>) -> ScratchArena<'static> { #[cfg(debug_assertions)] let conflict = conflict.map(|a| a.delegate_target_unchecked()); #[cold] fn init(s: &[Cell; 2]) { let capacity = INIT_SIZE.load(Ordering::Relaxed); for s in s { s.set(release::Arena::new(capacity).unwrap()); } } S_SCRATCH.with(|arenas| { let index = ptr::eq(opt_ptr(conflict), arenas[0].as_ptr()) as usize; let arena = unsafe { &*arenas[index].as_ptr() }; if arena.is_empty() { init(arenas); } ScratchArena::new(arena) }) } } #[cfg(not(feature = "single-threaded"))] pub use multi_threaded::*; #[cfg(feature = "single-threaded")] pub use single_threaded::*; ================================================ FILE: crates/stdext/src/collections/mod.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. mod string; mod vec; pub use string::{BString, BStringFormatter}; pub use vec::BVec; ================================================ FILE: crates/stdext/src/collections/string.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use std::fmt::{self}; use std::ops::{Bound, Deref, DerefMut, RangeBounds}; use std::slice; use std::str::Utf8Error; use crate::alloc::Allocator; use crate::cold_path; use crate::collections::BVec; /// Like a `String` but on borrowed memory. Built on top of [`BVec`]. pub struct BString<'a> { vec: BVec<'a, u8>, } impl<'a> BString<'a> { /// The label on the tin says "empty". You open it. It's empty. #[inline] pub const fn empty() -> Self { Self { vec: BVec::empty() } } /// See [`BVec::from_std_vec()`]. pub fn from_std_string(str: String) -> Self { Self { vec: BVec::from_std_vec(str.into_bytes()) } } /// See [`BVec::into_std_vec()`]. pub fn into_std_string(self) -> String { unsafe { String::from_utf8_unchecked(self.vec.into_std_vec()) } } /// Validates and wraps a byte vec as UTF-8. pub fn from_utf8(vec: BVec<'a, u8>) -> Result { str::from_utf8(&vec)?; Ok(Self { vec }) } /// Validates UTF-8, replacing invalid sequences with U+FFFD. pub fn from_utf8_lossy(alloc: &'a dyn Allocator, vec: BVec<'a, u8>) -> Self { let mut iter = vec.utf8_chunks(); if let Some(mut chunk) = iter.next() && !chunk.invalid().is_empty() { // We only need to create a copy if the input is non-empty // and contains at least some invalid UTF-8. cold_path(); let mut res = Self::empty(); res.reserve(alloc, vec.len()); loop { res.push_str(alloc, chunk.valid()); if !chunk.invalid().is_empty() { res.push_str(alloc, "\u{FFFD}"); } chunk = match iter.next() { Some(chunk) => chunk, None => break, }; } res } else { // Otherwise, we can just return the `vec` as-is. Self { vec } } } /// Wraps a byte vec as UTF-8 without validating it. /// /// # Safety /// /// The bytes in `vec` must be valid UTF-8. #[inline] pub unsafe fn from_utf8_unchecked(vec: BVec<'a, u8>) -> Self { Self { vec } } /// Copies `&str` into the allocator. pub fn from_str(alloc: &'a dyn Allocator, s: &str) -> Self { let mut res = Self::empty(); res.push_str(alloc, s); res } /// Decodes UTF-16, replacing unpaired surrogates with U+FFFD. pub fn from_utf16_lossy(alloc: &'a dyn Allocator, string: &[u16]) -> Self { let mut res = Self::empty(); res.push_utf16_lossy(alloc, string); res } /// Length in bytes, not characters. #[inline] pub fn len(&self) -> usize { self.vec.len() } /// Total byte capacity of the backing buffer. #[inline] pub fn capacity(&self) -> usize { self.vec.capacity() } /// True if the string is empty. #[inline] pub fn is_empty(&self) -> bool { self.vec.is_empty() } /// The raw UTF-8 bytes. #[inline] pub fn as_bytes(&self) -> &[u8] { self.vec.as_slice() } /// View as a `&str`. #[inline] pub fn as_str(&self) -> &str { unsafe { str::from_utf8_unchecked(self.vec.as_slice()) } } /// View as a `&mut str`. #[inline] pub fn as_mut_str(&mut self) -> &mut str { unsafe { str::from_utf8_unchecked_mut(self.vec.as_mut_slice()) } } /// # Safety /// /// The underlying `&mut Vec` allows writing bytes which are not valid UTF-8. #[inline] pub unsafe fn as_mut_vec(&mut self) -> &mut BVec<'a, u8> { &mut self.vec } /// Consume the string, returning a `&mut str` that lives as long as the borrowed memory. #[inline] pub fn leak(self) -> &'a mut str { unsafe { str::from_utf8_unchecked_mut(self.vec.leak()) } } /// Ensures space for at least `additional` more bytes, with amortized growth. #[inline] pub fn reserve(&mut self, alloc: &'a dyn Allocator, additional: usize) { self.vec.reserve(alloc, additional); } /// Ensures space for at least `additional` more bytes, without over-allocating. #[inline] pub fn reserve_exact(&mut self, arena: &'a dyn Allocator, additional: usize) { self.vec.reserve_exact(arena, additional); } /// Appends a single `char`, encoding it as UTF-8. pub fn push(&mut self, alloc: &'a dyn Allocator, ch: char) { self.reserve(alloc, 4); unsafe { let len = self.vec.len(); let dst = self.vec.as_mut_ptr().add(len); let add = ch.encode_utf8(slice::from_raw_parts_mut(dst, 4)).len(); self.vec.set_len(len + add); } } /// Empties the string. The allocation is kept. pub fn clear(&mut self) { self.vec.clear(); } /// Returns a [`BorrowedStringFormatter`] pairing this string with an allocator, /// enabling use with `write!` and `fmt::Write`. pub fn formatter(&mut self, alloc: &'a A) -> BStringFormatter<'_, 'a, A> where A: Allocator, { BStringFormatter { string: self, alloc } } /// Appends a `&str`. pub fn push_str(&mut self, alloc: &'a dyn Allocator, string: &str) { self.vec.extend_from_slice(alloc, string.as_bytes()); } /// Appends a UTF-16 slice, replacing unpaired surrogates with U+FFFD. pub fn push_utf16_lossy(&mut self, alloc: &'a dyn Allocator, string: &[u16]) { self.extend( alloc, char::decode_utf16(string.iter().cloned()) .map(|r| r.unwrap_or(char::REPLACEMENT_CHARACTER)), ); } /// Same as `push(char)` but with a specified number of character copies. /// Shockingly absent from the standard library. pub fn push_repeat(&mut self, alloc: &'a dyn Allocator, 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.push_repeat(alloc, 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(alloc, added_len); buf.extend_from_slice(alloc, utf8); while buf.len() != final_len { let end = (final_len - buf.len() + initial_len).min(buf.len()); buf.extend_from_within(alloc, initial_len..end); } } } /// Appends each `char` from the iterator. pub fn extend(&mut self, alloc: &'a dyn Allocator, iter: I) where I: IntoIterator, { let iterator = iter.into_iter(); let (lower_bound, _) = iterator.size_hint(); self.reserve(alloc, lower_bound); iterator.for_each(move |c| self.push(alloc, c)); } /// Replaces a range of characters with a new string. pub fn replace_range>( &mut self, alloc: &'a dyn Allocator, 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(alloc, 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, alloc: &'a dyn Allocator, old: &str, new: &str) { if let Some(beg) = self.find(old) { unsafe { self.as_mut_vec().replace_range(alloc, beg..beg + old.len(), new.as_bytes()) }; } } } impl Default for BString<'_> { fn default() -> Self { Self::empty() } } impl Deref for BString<'_> { type Target = str; #[inline] fn deref(&self) -> &str { self.as_str() } } impl DerefMut for BString<'_> { #[inline] fn deref_mut(&mut self) -> &mut str { self.as_mut_str() } } impl PartialEq> for BString<'_> { #[inline] fn eq(&self, other: &BString) -> bool { self.as_str() == other.as_str() } } impl Eq for BString<'_> {} impl PartialEq<&str> for BString<'_> { #[inline] fn eq(&self, other: &&str) -> bool { self.as_str() == *other } } impl PartialOrd for BString<'_> { #[inline] fn partial_cmp(&self, other: &BString) -> Option { Some(self.cmp(other)) } } impl Ord for BString<'_> { #[inline] fn cmp(&self, other: &BString) -> std::cmp::Ordering { self.as_str().cmp(other.as_str()) } } impl fmt::Debug for BString<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(self.as_str(), f) } } impl fmt::Display for BString<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(self.as_str(), f) } } /// Pairs a [`BString`] with an allocator so you can use `write!` on it. // NOTE: This struct uses a generic allocator, because I found that it shrinks the binary by 3KB somehow. // I never investigated why that is, or what the impact of that is, but it can't be good. // It does kind of make sense though, since this struct is generally temporary only. pub struct BStringFormatter<'s, 'a, A> { string: &'s mut BString<'a>, alloc: &'a A, } impl fmt::Write for BStringFormatter<'_, '_, A> where A: Allocator, { #[inline] fn write_str(&mut self, s: &str) -> fmt::Result { self.string.push_str(self.alloc, s); Ok(()) } #[inline] fn write_char(&mut self, c: char) -> fmt::Result { self.string.push(self.alloc, c); Ok(()) } } ================================================ FILE: crates/stdext/src/collections/vec.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use std::hint::assert_unchecked; use std::iter::FusedIterator; use std::marker::PhantomData; use std::mem::MaybeUninit; use std::ops::{Bound, Deref, DerefMut, Range, RangeBounds}; use std::ptr::{self, NonNull}; use std::{fmt, slice}; use crate::alloc::Allocator; #[cfg(debug_assertions)] use crate::alloc::GlobalAllocator; use crate::simd::memset; /// Similar to slices in Go, this slice has an additional capacity field. /// It allows you to push more elements into the slice beyond its length, /// up to the capacity. Like a `Vec` but on borrowed memory. /// /// # Safety /// /// The struct does not drop the elements, nor does it deallocate any memory. pub struct BVec<'a, T> { // NOTE: Only the first `len` elemennts are `T`, the rest are essentially `MaybeUninit`. // This is an important distinction, due to Rust's highly nebulous rules around uninitialized memory. // You should avoid `self.ptr.as_ptr().add(self.len)` and use `self.spare_mut_ptr()` instead. ptr: NonNull, len: usize, cap: usize, _marker: PhantomData<&'a T>, #[cfg(debug_assertions)] alloc: Option<&'a dyn Allocator>, } impl<'a, T> BVec<'a, T> { /// The label on the tin says "empty". You open it. It's empty. #[inline] pub const fn empty() -> Self { Self { ptr: NonNull::dangling(), len: 0, cap: 0, _marker: PhantomData, #[cfg(debug_assertions)] alloc: None, } } pub fn from_slice(slice: &'a mut [T]) -> Self { let slice = NonNull::from_mut(slice); Self { ptr: slice.cast(), len: slice.len(), cap: slice.len(), _marker: PhantomData, #[cfg(debug_assertions)] alloc: None, } } /// Leaks a `Vec` and turns it into a "borrowed" `BVec`. pub fn from_std_vec(vec: Vec) -> Self { let (ptr, len, cap) = vec.into_raw_parts(); // A `Vec` always has a non-null pointer (it's dangling). let ptr = unsafe { NonNull::new_unchecked(ptr) }; Self { ptr, len, cap, _marker: PhantomData, #[cfg(debug_assertions)] alloc: Some(&GlobalAllocator), } } /// Under the assumption that your `BVec` uses `GlobalAlloc`, /// this turns it back into a standard `Vec`. /// /// It's not marked as `unsafe`, because people count the "unsafe" keyword as a measure of safety /// the way managers count lines of code to measure productivity. So, by not marking it as unsafe, /// I've effectively improved the security of this project. The "Real Men of Genius" ad plays in my head. /// /// In all seriousness though, there are debug runtime checks. That's sufficient for my purpose. pub fn into_std_vec(self) -> Vec { #[cfg(debug_assertions)] debug_assert!( self.alloc.is_none_or(|a| std::ptr::eq(a, &GlobalAllocator)), "BVec can only be converted into Vec if it was allocated with GlobalAlloc" ); unsafe { Vec::from_raw_parts(self.ptr.as_ptr(), self.len, self.cap) } } /// Number of initialized elements. #[inline] pub fn len(&self) -> usize { self.len } /// Total number of elements the buffer can hold. #[inline] pub fn capacity(&self) -> usize { self.cap } /// True if there are zero elements. #[inline] pub fn is_empty(&self) -> bool { self.len == 0 } /// Forcibly sets the length. /// /// # Safety /// /// The first `new_len` items must be initialized. /// Items beyond `new_len` are not dropped when you call `set_len()`. #[inline] pub unsafe fn set_len(&mut self, new_len: usize) { debug_assert!(new_len <= self.cap); self.len = new_len; } /// Shortens the vector. pub fn truncate(&mut self, len: usize) { unsafe { // NOTE: It's intentional that this doesn't avoid drops when `len == self.len`, // because that would introduce a branch for the common case of `truncate(0)`. if let Some(r) = self.len.checked_sub(len) { let s = ptr::slice_from_raw_parts_mut(self.as_mut_ptr().add(len), r); self.len = len; ptr::drop_in_place(s); } } } /// Raw pointer to the backing buffer. #[inline] pub fn as_ptr(&self) -> *const T { self.ptr.as_ptr() } /// Mutable raw pointer to the backing buffer. #[inline] pub fn as_mut_ptr(&mut self) -> *mut T { self.ptr.as_ptr() } #[inline] fn spare_mut_ptr(&mut self) -> *mut MaybeUninit { unsafe { (self.ptr.as_ptr() as *mut MaybeUninit).add(self.len) } } /// View as a shared slice. #[inline] pub fn as_slice(&self) -> &[T] { unsafe { slice::from_raw_parts(self.ptr.as_ptr(), self.len) } } /// View as a mutable slice. #[inline] pub fn as_mut_slice(&mut self) -> &mut [T] { unsafe { slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) } } /// Consume the string, returning a `&mut [T]` that lives as long as the borrowed memory. #[inline] pub fn leak(self) -> &'a mut [T] { unsafe { slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) } } /// Drops all elements and resets length to zero. The allocation is kept. #[inline] pub fn clear(&mut self) { let elems = self.as_mut_slice() as *mut _; self.len = 0; unsafe { ptr::drop_in_place(elems) }; } /// Ensures space for at least `additional` more elements, with amortized growth. #[inline] pub fn reserve(&mut self, alloc: &'a dyn Allocator, additional: usize) { let len = self.len; let cap = self.cap; if additional > cap - len { self.grow(alloc, self.cap, additional); } unsafe { // Right now the following asserts are somewhat useless, because they only work // if grow() is inline(never). I don't know why that is either. But I'm leaving // them here, in case we need them in the future - they don't hurt until then. // First, we can tell the compiler that re-fetching self.len after grow() is unnecessary. assert_unchecked(self.len == len); // Next, we can assert that after reserve(4), we have room for 4 more elements. // Naively you'd expect this to be `self.len + additional <= self.cap`, but LLVM doesn't // work very well with `<=` bounds, so we use `<` here. It _must_ be `additional - 1`. assert_unchecked(additional == 0 || self.len.unchecked_add(additional - 1) < self.cap); } } /// Ensures space for at least `additional` more elements, without over-allocating. #[inline] pub fn reserve_exact(&mut self, alloc: &'a dyn Allocator, additional: usize) { let len = self.len; let cap = self.cap; if additional > cap - len { self.grow(alloc, 0, additional); } unsafe { // See reserve(). assert_unchecked(self.len == len); assert_unchecked(additional == 0 || self.len.unchecked_add(additional - 1) < self.cap); } } #[inline] fn reserve_one(&mut self, alloc: &'a dyn Allocator) { let len = self.len; let cap = self.cap; if len >= cap { self.grow(alloc, cap, 1); } unsafe { // See reserve(). assert_unchecked(self.len == len); assert_unchecked(self.len < self.cap); } } #[cold] fn grow(&mut self, alloc: &'a dyn Allocator, cap: usize, add: usize) { debug_assert!(add > 0, "growing by zero makes no sense"); #[cfg(debug_assertions)] debug_assert!( self.alloc.is_none_or(|a| std::ptr::eq(a, alloc)), "switching between allocators on a single BVec heavily suggests you're about to leak memory" ); let new_cap = (cap * 2).max(self.len + add).max(8); let new_ptr = unsafe { alloc.realloc( self.ptr.cast(), self.cap * size_of::(), new_cap * size_of::(), align_of::(), ) }; self.ptr = new_ptr.cast(); self.cap = new_ptr.len() / size_of::(); } /// Returns the uninitialized tail of the buffer. Fill it, then `set_len()`. pub fn spare_capacity_mut(&mut self) -> &mut [MaybeUninit] { unsafe { slice::from_raw_parts_mut(self.spare_mut_ptr(), self.cap - self.len) } } /// Appends one element, returning a mutable reference to it. pub fn push(&mut self, alloc: &'a dyn Allocator, value: T) -> &mut T { self.reserve_one(alloc); unsafe { let dst = self.spare_mut_ptr(); self.len += 1; (*dst).write(value) } } pub fn pop(&mut self) -> Option { if self.is_empty() { return None; } unsafe { self.len -= 1; // See: https://github.com/rust-lang/rust/issues/114334 // This assert helps the optimizer understand that // after a pop it can push once without reallocating. assert_unchecked(self.len < self.cap); Some(self.as_ptr().add(self.len).read()) } } /// Append the items from the iterator `iter`. /// /// By assuming that your "exact size iterator" returns an *exact* size, /// it can preallocate the memory in one go and efficiently push items. pub fn extend(&mut self, alloc: &'a dyn Allocator, iter: I) where I: IntoIterator + ExactSizeIterator, { let len = iter.len(); self.reserve(alloc, len); unsafe { let mut dst = self.spare_mut_ptr(); self.len += len; for value in iter { (*dst).write(value); dst = dst.add(1); } } } /// This is the bad path of `extend()`. It has a distinct name, because it makes /// it easy to find. If you use this method, you're not writing ideal code. pub fn extend_sloppy(&mut self, alloc: &'a dyn Allocator, iter: I) where I: IntoIterator, { let iterator = iter.into_iter(); let (lower_bound, _) = iterator.size_hint(); self.reserve(alloc, lower_bound); iterator.for_each(move |c| _ = self.push(alloc, c)); } } impl<'a, T: Copy> BVec<'a, T> { /// Pushes `total_copies` copies of `value`. It's basically `memset`. pub fn push_repeat(&mut self, alloc: &'a dyn Allocator, value: T, total_copies: usize) { if total_copies == 0 { return; } self.reserve(alloc, total_copies); unsafe { let dst = slice::from_raw_parts_mut(self.spare_mut_ptr(), total_copies); self.len += total_copies; // Increment first, to turn memset() into a tail call memset(dst, MaybeUninit::new(value)); } } /// Appends all elements from a slice. It's basically a `memcpy`-append. #[allow(clippy::mut_from_ref)] pub fn extend_from_slice(&mut self, alloc: &'a dyn Allocator, other: &[T]) { let add = other.len(); self.reserve(alloc, add); unsafe { let dst = self.spare_mut_ptr(); self.len += add; ptr::copy_nonoverlapping(other.as_ptr() as *const _, dst, add); } } /// [`Self::extend_from_slice`] but for a subslice of the buffer itself. #[inline] pub fn extend_from_within(&mut self, alloc: &'a dyn Allocator, src: R) where R: RangeBounds, { let start = match src.start_bound() { Bound::Included(&start) => start, Bound::Excluded(start) => start + 1, Bound::Unbounded => 0, }; let end = match src.end_bound() { Bound::Included(end) => end + 1, Bound::Excluded(&end) => end, Bound::Unbounded => usize::MAX, }; self.extend_from_within_impl(alloc, start..end); } fn extend_from_within_impl(&mut self, alloc: &'a dyn Allocator, src: Range) { let end = src.end.min(self.len); let beg = src.start.min(end); let add = end - beg; self.reserve(alloc, add); unsafe { let dst = self.spare_mut_ptr(); let src = self.ptr.as_ptr().add(beg); self.len += add; ptr::copy_nonoverlapping(src as *const _, dst, add); } } /// Replaces the given range with elements from `src`. Efficient `splice` for `Copy` types. #[inline] pub fn replace_range(&mut self, alloc: &'a dyn Allocator, range: R, src: &[T]) where R: RangeBounds, { 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, }; self.replace_range_impl(alloc, start..end, src); } // At the time of writing, this implementation of what's // essentially `Vec::splice` is vastly more efficient. fn replace_range_impl(&mut self, alloc: &'a dyn Allocator, range: Range, src: &[T]) { unsafe { let dst_len = self.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 { self.reserve(alloc, 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 = self.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); self.set_len(new_len); } } } #[cfg(windows)] unsafe extern "system" { fn MultiByteToWideChar( CodePage: u32, dwFlags: u32, lpMultiByteStr: *const u8, cbMultiByte: i32, lpWideCharStr: *mut u16, cchWideChar: i32, ) -> i32; } impl<'a> BVec<'a, u16> { pub fn push_encode_utf16(&mut self, alloc: &'a dyn Allocator, utf8: &[u8]) { self.reserve(alloc, utf8.len()); // worst case ASCII: 1 byte per char // MultiByteToWideChar is ~2x faster than the UTF8 loop below and saves space. #[cfg(windows)] unsafe { let dst = self.spare_mut_ptr() as *mut u16; let len = MultiByteToWideChar( 65001, 0, utf8.as_ptr(), utf8.len() as i32, dst, utf8.len() as i32, ); self.len += len.max(0) as usize; } #[cfg(not(windows))] unsafe { let beg = self.spare_mut_ptr(); let mut dst = beg; for ch in crate::unicode::Utf8Chars::new(utf8, 0) { if ch <= '\u{FFFF}' { (*dst).write(ch as u16); dst = dst.add(1); } else { let ch = ch as u32 - 0x10000; (*dst.add(0)).write(0xD800 | ((ch >> 10) as u16)); (*dst.add(1)).write(0xDC00 | ((ch as u16) & 0x3FF)); dst = dst.add(2); } } self.len += dst.offset_from_unsigned(beg); } } } impl Default for BVec<'_, T> { fn default() -> Self { Self::empty() } } impl Deref for BVec<'_, T> { type Target = [T]; #[inline] fn deref(&self) -> &[T] { self.as_slice() } } impl DerefMut for BVec<'_, T> { #[inline] fn deref_mut(&mut self) -> &mut [T] { self.as_mut_slice() } } impl PartialEq> for BVec<'_, T> where T: PartialEq, { #[inline] fn eq(&self, other: &BVec) -> bool { self.deref() == other.deref() } } impl Eq for BVec<'_, T> where T: PartialEq {} impl PartialEq<[T]> for BVec<'_, T> where T: PartialEq, { #[inline] fn eq(&self, other: &[T]) -> bool { self.deref() == other } } impl PartialOrd for BVec<'_, T> where T: PartialOrd, { #[inline] fn partial_cmp(&self, other: &BVec) -> Option { self.deref().partial_cmp(other.deref()) } } impl Ord for BVec<'_, T> where T: Ord, { #[inline] fn cmp(&self, other: &BVec) -> std::cmp::Ordering { self.deref().cmp(other.deref()) } } impl fmt::Debug for BVec<'_, T> where T: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(self.deref(), f) } } impl<'a, T> IntoIterator for BVec<'a, T> { type Item = T; type IntoIter = IntoIter<'a, T>; #[inline] fn into_iter(self) -> Self::IntoIter { unsafe { let ptr = self.ptr; let end = ptr.add(self.len); IntoIter { ptr, end, phantom: PhantomData } } } } impl<'a, T> IntoIterator for &'a BVec<'a, T> { type Item = &'a T; type IntoIter = slice::Iter<'a, T>; #[inline] fn into_iter(self) -> Self::IntoIter { self.iter() } } impl<'a, T> IntoIterator for &'a mut BVec<'a, T> { type Item = &'a mut T; type IntoIter = slice::IterMut<'a, T>; #[inline] fn into_iter(self) -> Self::IntoIter { self.iter_mut() } } /// Owning iterator over the elements of a [`BVec`]. pub struct IntoIter<'a, T> { ptr: NonNull, end: NonNull, phantom: PhantomData<&'a T>, } impl<'a, T> Iterator for IntoIter<'a, T> { type Item = T; #[inline] fn next(&mut self) -> Option { if self.ptr == self.end { return None; } let ptr = self.ptr; self.ptr = unsafe { ptr.add(1) }; Some(unsafe { ptr.read() }) } #[inline] fn size_hint(&self) -> (usize, Option) { let len = self.len(); (len, Some(len)) } #[inline] fn count(self) -> usize { self.len() } #[inline] fn last(mut self) -> Option { self.next_back() } #[inline] fn nth(&mut self, n: usize) -> Option { if n >= self.len() { self.ptr = self.end; return None; } let ptr = self.ptr; self.ptr = unsafe { ptr.add(n + 1) }; Some(unsafe { ptr.read() }) } fn fold(mut self, mut accum: B, mut f: F) -> B where F: FnMut(B, Self::Item) -> B, { while self.ptr != self.end { let ptr = self.ptr; self.ptr = unsafe { ptr.add(1) }; accum = f(accum, unsafe { self.ptr.read() }); } accum } } impl<'a, T> DoubleEndedIterator for IntoIter<'a, T> { #[inline] fn next_back(&mut self) -> Option { if self.ptr == self.end { return None; } unsafe { self.end = self.end.sub(1); Some(self.end.read()) } } } impl<'a, T> ExactSizeIterator for IntoIter<'a, T> { #[inline] fn len(&self) -> usize { unsafe { self.end.offset_from_unsigned(self.ptr) } } } impl<'a, T> FusedIterator for IntoIter<'a, T> {} ================================================ FILE: crates/stdext/src/helpers.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! Random assortment of helpers I didn't know where to put. use std::borrow::Cow; use std::mem::{self, MaybeUninit}; use std::ops::{Bound, Range, RangeBounds}; use std::{fmt, ptr, slice, str}; 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(pub T); impl fmt::Display for MetricFormatter { 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}") } } #[inline(always)] #[cold] pub const fn cold_path() {} /// [`std::cmp::minmax`] is unstable, as per usual. pub fn minmax(v1: T, v2: T) -> [T; 2] where T: Ord, { if v2 < v1 { [v2, v1] } else { [v1, v2] } } #[inline(always)] #[allow(clippy::ptr_eq)] pub fn opt_ptr(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(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(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 { fn replace_range>(&mut self, range: R, src: &[T]); } impl ReplaceRange for Vec { fn replace_range>(&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(dst: &mut Vec, range: Range, 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); } } /// Turns a [`&[u8]`] into a [`&[MaybeUninit]`]. #[inline(always)] pub const fn slice_as_uninit_ref(slice: &[T]) -> &[MaybeUninit] { unsafe { slice::from_raw_parts(slice.as_ptr() as *const MaybeUninit, slice.len()) } } /// Turns a [`&mut [T]`] into a [`&mut [MaybeUninit]`]. #[inline(always)] pub const fn slice_as_uninit_mut(slice: &mut [T]) -> &mut [MaybeUninit] { unsafe { slice::from_raw_parts_mut(slice.as_mut_ptr() as *mut MaybeUninit, slice.len()) } } /// A stable clone of [`String::from_utf8_lossy_owned`] (`string_from_utf8_lossy_owned`). pub fn string_from_utf8_lossy_owned(v: Vec) -> String { if let Cow::Owned(string) = String::from_utf8_lossy(&v) { string } else { unsafe { String::from_utf8_unchecked(v) } } } /// Helpers for ASCII string comparisons. pub trait AsciiStringHelpers { /// Tests if a string starts with a given ASCII prefix. /// /// This function name really is a mouthful, but it's a combination /// of [`str::starts_with`] and [`str::eq_ignore_ascii_case`]. fn starts_with_ignore_ascii_case(&self, prefix: &str) -> bool; } impl AsciiStringHelpers for str { fn starts_with_ignore_ascii_case(&self, prefix: &str) -> bool { // Casting to bytes first ensures we skip any UTF8 boundary checks. // Since the comparison is ASCII, we don't need to worry about that. let s = self.as_bytes(); let p = prefix.as_bytes(); p.len() <= s.len() && s[..p.len()].eq_ignore_ascii_case(p) } } ================================================ FILE: crates/stdext/src/lib.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! Arena allocators. Small and fast. pub mod alloc; pub mod arena; pub mod collections; mod helpers; pub mod simd; pub mod sys; pub mod unicode; pub use helpers::*; ================================================ FILE: crates/stdext/src/simd/memset.rs ================================================ // 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; /// Fills a slice with the given value. #[inline(always)] pub fn memset(dst: &mut [T], val: T) { unsafe { match mem::size_of::() { 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); } _ => dst.fill(val), } } } #[inline(always)] fn memset_raw(beg: *mut u8, end: *mut u8, val: u64) { #[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "loongarch64"))] return unsafe { MEMSET_DISPATCH(beg, end, val) }; #[cfg(target_arch = "aarch64")] return unsafe { memset_neon(beg, end, val) }; #[allow(unreachable_code)] return unsafe { memset_fallback(beg, end, val) }; } #[inline(never)] unsafe fn memset_fallback(mut beg: *mut u8, end: *mut u8, val: u64) { unsafe { let mut remaining = end.offset_from_unsigned(beg); while remaining >= 8 { (beg as *mut u64).write_unaligned(val); beg = beg.add(8); remaining -= 8; } 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_arch = "loongarch64"))] static mut MEMSET_DISPATCH: unsafe fn(beg: *mut u8, end: *mut u8, val: u64) = memset_dispatch; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] 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 = end.offset_from_unsigned(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 = end.offset_from_unsigned(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 = "loongarch64")] fn memset_dispatch(beg: *mut u8, end: *mut u8, val: u64) { use std::arch::is_loongarch_feature_detected; let func = if is_loongarch_feature_detected!("lasx") { memset_lasx } else if is_loongarch_feature_detected!("lsx") { memset_lsx } else { memset_fallback }; unsafe { MEMSET_DISPATCH = func }; unsafe { func(beg, end, val) } } #[cfg(target_arch = "loongarch64")] #[target_feature(enable = "lasx")] fn memset_lasx(mut beg: *mut u8, end: *mut u8, val: u64) { unsafe { use std::arch::loongarch64::*; let fill = lasx_xvreplgr2vr_d(val as i64); if end.offset_from_unsigned(beg) >= 32 { lasx_xvst::<0>(fill, beg as *mut _); let off = beg.align_offset(32); beg = beg.add(off); } if end.offset_from_unsigned(beg) >= 128 { loop { lasx_xvst::<0>(fill, beg as *mut _); lasx_xvst::<32>(fill, beg as *mut _); lasx_xvst::<64>(fill, beg as *mut _); lasx_xvst::<96>(fill, beg as *mut _); beg = beg.add(128); if end.offset_from_unsigned(beg) < 128 { break; } } } if end.offset_from_unsigned(beg) >= 16 { let fill = lsx_vreplgr2vr_d(val as i64); loop { lsx_vst::<0>(fill, beg as *mut _); beg = beg.add(16); if end.offset_from_unsigned(beg) < 16 { break; } } } if end.offset_from_unsigned(beg) >= 8 { // 8-15 bytes (beg as *mut u64).write_unaligned(val); (end.sub(8) as *mut u64).write_unaligned(val); } else if end.offset_from_unsigned(beg) >= 4 { // 4-7 bytes (beg as *mut u32).write_unaligned(val as u32); (end.sub(4) as *mut u32).write_unaligned(val as u32); } else if end.offset_from_unsigned(beg) >= 2 { // 2-3 bytes (beg as *mut u16).write_unaligned(val as u16); (end.sub(2) as *mut u16).write_unaligned(val as u16); } else if end.offset_from_unsigned(beg) >= 1 { // 1 byte beg.write(val as u8); } } } #[cfg(target_arch = "loongarch64")] #[target_feature(enable = "lsx")] unsafe fn memset_lsx(mut beg: *mut u8, end: *mut u8, val: u64) { unsafe { use std::arch::loongarch64::*; if end.offset_from_unsigned(beg) >= 16 { let fill = lsx_vreplgr2vr_d(val as i64); lsx_vst::<0>(fill, beg as *mut _); let off = beg.align_offset(16); beg = beg.add(off); while end.offset_from_unsigned(beg) >= 32 { lsx_vst::<0>(fill, beg as *mut _); lsx_vst::<16>(fill, beg as *mut _); beg = beg.add(32); } if end.offset_from_unsigned(beg) >= 16 { // 16-31 bytes remaining lsx_vst::<0>(fill, beg as *mut _); lsx_vst::<-16>(fill, end as *mut _); return; } } if end.offset_from_unsigned(beg) >= 8 { // 8-15 bytes remaining (beg as *mut u64).write_unaligned(val); (end.sub(8) as *mut u64).write_unaligned(val); } else if end.offset_from_unsigned(beg) >= 4 { // 4-7 bytes remaining (beg as *mut u32).write_unaligned(val as u32); (end.sub(4) as *mut u32).write_unaligned(val as u32); } else if end.offset_from_unsigned(beg) >= 2 { // 2-3 bytes remaining (beg as *mut u16).write_unaligned(val as u16); (end.sub(2) as *mut u16).write_unaligned(val as u16); } else if end.offset_from_unsigned(beg) >= 1 { // 1 byte remaining beg.write(val as u8); } } } #[cfg(target_arch = "aarch64")] unsafe fn memset_neon(mut beg: *mut u8, end: *mut u8, val: u64) { unsafe { use std::arch::aarch64::*; let mut remaining = end.offset_from_unsigned(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(val: T, len: usize) where T: Copy + Not + 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)); } } } ================================================ FILE: crates/stdext/src/simd/mod.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. mod memset; pub use memset::*; ================================================ FILE: crates/stdext/src/sys/mod.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! Platform abstractions. #[cfg(unix)] mod unix; #[cfg(windows)] mod windows; #[cfg(not(windows))] pub use std::fs::canonicalize; #[cfg(unix)] pub use unix::*; #[cfg(windows)] pub use windows::*; ================================================ FILE: crates/stdext/src/sys/unix.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use std::ffi::c_int; use std::io; use std::ptr::{self, NonNull, null_mut}; /// Reserves a virtual memory region of the given size. /// To commit the memory, use `virtual_commit`. /// To release the memory, use `virtual_release`. /// /// # Safety /// /// This function is unsafe because it uses raw pointers. /// Don't forget to release the memory when you're done with it or you'll leak it. pub unsafe fn virtual_reserve(size: usize) -> io::Result> { unsafe { let ptr = libc::mmap( null_mut(), size, desired_mprotect(libc::PROT_READ | libc::PROT_WRITE), libc::MAP_PRIVATE | libc::MAP_ANONYMOUS, -1, 0, ); if ptr.is_null() || ptr::eq(ptr, libc::MAP_FAILED) { Err(io::Error::last_os_error()) } else { Ok(NonNull::new_unchecked(ptr as *mut u8)) } } } #[cfg(target_os = "netbsd")] const fn desired_mprotect(flags: c_int) -> c_int { // NetBSD allows an mmap(2) caller to specify what protection flags they // will use later via mprotect. It does not allow a caller to move from // PROT_NONE to PROT_READ | PROT_WRITE. // // see PROT_MPROTECT in man 2 mmap flags << 3 } #[cfg(not(target_os = "netbsd"))] const fn desired_mprotect(_: c_int) -> c_int { libc::PROT_NONE } /// Releases a virtual memory region of the given size. /// /// # Safety /// /// This function is unsafe because it uses raw pointers. /// Make sure to only pass pointers acquired from `virtual_reserve`. pub unsafe fn virtual_release(base: NonNull, 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, size: usize) -> io::Result<()> { unsafe { let status = libc::mprotect(base.cast().as_ptr(), size, libc::PROT_READ | libc::PROT_WRITE); if status != 0 { Err(io::Error::last_os_error()) } else { Ok(()) } } } ================================================ FILE: crates/stdext/src/sys/windows.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use std::io; use std::ptr::{NonNull, null_mut}; const MEM_COMMIT: u32 = 0x00001000; const MEM_RELEASE: u32 = 0x00008000; const MEM_RESERVE: u32 = 0x00002000; const PAGE_READWRITE: u32 = 0x04; unsafe extern "system" { fn VirtualAlloc( lpAddress: *mut u8, dwSize: usize, flAllocationType: u32, flProtect: u32, ) -> *mut u8; fn VirtualFree(lpAddress: *mut u8, dwSize: usize, dwFreeType: u32) -> i32; } /// Reserves a virtual memory region of the given size. /// To commit the memory, use [`virtual_commit`]. /// To release the memory, use [`virtual_release`]. /// /// # Safety /// /// This function is unsafe because it uses raw pointers. /// Don't forget to release the memory when you're done with it or you'll leak it. pub unsafe fn virtual_reserve(size: usize) -> io::Result> { unsafe { let res = VirtualAlloc(null_mut(), size, MEM_RESERVE, PAGE_READWRITE); if res.is_null() { Err(io::Error::last_os_error()) } else { Ok(NonNull::new_unchecked(res)) } } } /// Releases a virtual memory region of the given size. /// /// # Safety /// /// This function is unsafe because it uses raw pointers. /// Make sure to only pass pointers acquired from [`virtual_reserve`]. pub unsafe fn virtual_release(base: NonNull, _size: usize) { unsafe { // NOTE: `VirtualFree` fails if the pointer isn't // a valid base address or if the size isn't zero. VirtualFree(base.as_ptr() as *mut _, 0, MEM_RELEASE); } } /// Commits a virtual memory region of the given size. /// /// # Safety /// /// This function is unsafe because it uses raw pointers. /// Make sure to only pass pointers acquired from [`virtual_reserve`] /// and to pass a size less than or equal to the size passed to [`virtual_reserve`]. pub unsafe fn virtual_commit(base: NonNull, size: usize) -> io::Result<()> { unsafe { let res = VirtualAlloc(base.as_ptr() as *mut _, size, MEM_COMMIT, PAGE_READWRITE); if res.is_null() { Err(io::Error::last_os_error()) } else { Ok(()) } } } ================================================ FILE: crates/stdext/src/unicode/mod.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //! Everything related to Unicode lives here. mod utf8; pub use utf8::*; ================================================ FILE: crates/stdext/src/unicode/utf8.rs ================================================ // 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. 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 { 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) { // 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); } } } } ================================================ FILE: crates/unicode-gen/Cargo.toml ================================================ [package] name = "unicode-gen" version = "0.0.0" edition.workspace = true license.workspace = true repository.workspace = true rust-version.workspace = true [dependencies] anyhow = "1.0" chrono = "0.4" indoc = "2.0" pico-args = { version = "0.5", features = ["eq-separator"] } rayon = "1.10" roxmltree = { version = "0.21", default-features = false, features = ["std"] } ================================================ FILE: crates/unicode-gen/README.md ================================================ # 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: crates/unicode-gen/src/main.rs ================================================ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. mod rules; use std::collections::HashMap; use std::fmt::Write as FmtWrite; use std::io::Write as IoWrite; use std::ops::RangeInclusive; use std::path::PathBuf; use anyhow::{Context, bail}; use indoc::writedoc; use rayon::prelude::*; use crate::rules::{JOIN_RULES_GRAPHEME_CLUSTER, JOIN_RULES_LINE_BREAK}; // `CharacterWidth` is 2 bits. #[derive(Clone, Copy, PartialEq, Eq)] enum CharacterWidth { ZeroWidth, Narrow, Wide, Ambiguous, } // `ClusterBreak` is 4 bits without `StartOfText`, 5 bits with it. // NOTE: The order of these items must match JOIN_RULES_GRAPHEME_CLUSTER. #[derive(Clone, Copy, PartialEq, Eq)] #[allow(clippy::upper_case_acronyms)] enum ClusterBreak { Other, // GB999 CR, // GB3, GB4, GB5 LF, // GB3, GB4, GB5 Control, // GB4, GB5 Extend, // GB9, GB9a -- includes SpacingMark RI, // GB12, GB13 Prepend, // GB9b HangulL, // GB6, GB7, GB8 HangulV, // GB6, GB7, GB8 HangulT, // GB6, GB7, GB8 HangulLV, // GB6, GB7, GB8 HangulLVT, // GB6, GB7, GB8 InCBLinker, // GB9c InCBConsonant, // GB9c ExtPic, // GB11 ZWJ, // GB9, GB11 } // Extended information for each `ClusterBreak` via --extended. // Currently only used for storing the subtype "tab" for `ClusterBreak::Control`. // As such, this is 1 bit. #[derive(Clone, Copy, PartialEq, Eq)] enum ClusterBreakExt { ControlTab = 1, } // `LineBreak` is 5 bits. // NOTE: The order of these items must match JOIN_RULES_LINE_BREAK. #[derive(Clone, Copy, PartialEq, Eq)] #[allow(non_camel_case_types)] enum LineBreak { Other, // Anything else // Non-tailorable Line Breaking Classes WordJoiner, // WJ ZeroWidthSpace, // ZW Glue, // GL Space, // SP // Break Opportunities BreakAfter, // BA BreakBefore, // BB Hyphen, // HY // Characters Prohibiting Certain Breaks ClosePunctuation, // CL CloseParenthesis_EA, // CP, East Asian CloseParenthesis_NotEA, // CP, not East Asian Exclamation, // EX Inseparable, // IN Nonstarter, // NS OpenPunctuation_EA, // OP, East Asian OpenPunctuation_NotEA, // OP, not East Asian Quotation, // QU // Numeric Context InfixNumericSeparator, // IS Numeric, // NU PostfixNumeric, // PO PrefixNumeric, // PR SymbolsAllowingBreakAfter, // SY // Other Characters Alphabetic, // AL & HL Ideographic, // ID & EB & EM StartOfText, // LB2 (optional via --extended) } #[repr(transparent)] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] struct TrieType(u32); impl TrieType { fn new(packing: &BitPacking, cb: ClusterBreak, lb: LineBreak, cw: CharacterWidth) -> Self { let cb = cb as u32; let lb = lb as u32; let cw = cw as u32; assert!(cb <= packing.mask_cluster_break); assert!(lb <= packing.mask_line_break); assert!(cw <= packing.mask_character_width); let cb = cb << packing.shift_cluster_break; let lb = lb << packing.shift_line_break; let cw = cw << packing.shift_character_width; Self(cb | lb | cw) } fn change_cluster_break_ext(&mut self, packing: &BitPacking, cbe: ClusterBreakExt) { let mask = packing.mask_cluster_break_ext; let shift = packing.shift_cluster_break_ext; let cbe = cbe as u32; assert!(cbe <= mask); self.0 = (self.0 & !(mask << shift)) | (cbe << shift); } fn change_width(&mut self, packing: &BitPacking, cw: CharacterWidth) { let mask = packing.mask_character_width; let shift = packing.shift_character_width; let cw = cw as u32; assert!(cw <= mask); self.0 = (self.0 & !(mask << shift)) | (cw << shift); } fn value(&self) -> u32 { self.0 } } #[derive(Default)] struct BitPacking { mask_cluster_break: u32, mask_cluster_break_ext: u32, mask_line_break: u32, mask_character_width: u32, shift_cluster_break: u32, shift_cluster_break_ext: u32, shift_line_break: u32, shift_character_width: u32, } impl BitPacking { fn new(line_breaks: bool, extended: bool) -> Self { let cb_width: u32 = if extended { 5 } else { 4 }; let cb_ext_width: u32 = if extended { 1 } else { 0 }; let lb_width: u32 = if line_breaks { 5 } else { 0 }; let cw_width: u32 = 3; Self { mask_cluster_break: (1 << cb_width) - 1, mask_cluster_break_ext: (1 << cb_ext_width) - 1, mask_line_break: (1 << lb_width) - 1, mask_character_width: (1 << cw_width) - 1, shift_cluster_break: 0, shift_cluster_break_ext: cb_width, shift_line_break: cb_width + cb_ext_width, shift_character_width: cb_width + cb_ext_width + lb_width, } } } #[derive(Default)] struct Ucd { description: String, values: Vec, packing: BitPacking, } #[derive(Clone, Default)] struct Stage { values: Vec, index: usize, shift: usize, mask: usize, bits: usize, } #[derive(Clone, Default)] struct Trie { stages: Vec, total_size: usize, } #[derive(Clone, Copy, Default)] enum Language { #[default] C, Rust, } #[derive(Default)] struct Output { arg_lang: Language, arg_extended: bool, arg_no_ambiguous: bool, arg_line_breaks: bool, ucd: Ucd, trie: Trie, rules_gc: Vec>, rules_lb: Vec, total_size: usize, } impl Output { fn args(&self) -> String { let mut buf = String::new(); match self.arg_lang { Language::C => buf.push_str("--lang=c"), Language::Rust => buf.push_str("--lang=rust"), } if self.arg_extended { buf.push_str(" --extended") } if self.arg_no_ambiguous { buf.push_str(" --no-ambiguous") } if self.arg_line_breaks { buf.push_str(" --line-breaks") } buf } } const HELP: &str = "\ Usage: grapheme-table-gen [options...] -h, --help Prints help information --lang= Output language (default: c) --extended Expose a start-of-text property for kick-starting the segmentation Expose tab and linefeed as grapheme cluster properties --no-ambiguous Treat all ambiguous characters as narrow --line-breaks Store and expose line break information Download ucd.nounihan.grouped.xml at: https://www.unicode.org/Public/UCD/latest/ucdxml/ucd.nounihan.grouped.zip "; fn main() -> anyhow::Result<()> { let mut args = pico_args::Arguments::from_env(); if args.contains(["-h", "--help"]) { eprint!("{HELP}"); return Ok(()); } let mut out = Output { arg_lang: args.value_from_fn("--lang", |arg| match arg { "c" => Ok(Language::C), "rust" => Ok(Language::Rust), l => bail!("invalid language: \"{}\"", l), })?, arg_extended: args.contains("--extended"), arg_no_ambiguous: args.contains("--no-ambiguous"), arg_line_breaks: args.contains("--line-breaks"), ..Default::default() }; let arg_input = args.free_from_os_str(|s| -> Result { Ok(s.into()) })?; let arg_remaining = args.finish(); if !arg_remaining.is_empty() { bail!("unrecognized arguments: {:?}", arg_remaining); } let input = std::fs::read_to_string(arg_input)?; let doc = roxmltree::Document::parse(&input)?; out.ucd = extract_values_from_ucd(&doc, &out)?; // Find the best trie configuration over the given block sizes (2^2 - 2^8) and stages (4). // More stages = Less size. The trajectory roughly follows a+b*c^stages, where c < 1. // 4 still gives ~30% savings over 3 stages and going beyond 5 gives diminishing returns (<10%). out.trie = build_best_trie(&out.ucd.values, 2, 8, 4); // The joinRules above has 2 bits per value. This packs it into 32-bit integers to save space. out.rules_gc = JOIN_RULES_GRAPHEME_CLUSTER .iter() .map(|t| { let rules_gc_len = if out.arg_extended { t.len() } else { 16 }; t[..rules_gc_len].iter().map(|row| prepare_rules_row(row, 2, 3)).collect() }) .collect(); // Same for line breaks, but in 2D. let rules_lb_len = if out.arg_extended { JOIN_RULES_LINE_BREAK.len() } else { 24 }; out.rules_lb = JOIN_RULES_LINE_BREAK[..rules_lb_len] .iter() .map(|row| prepare_rules_row(row, 1, 0)) .collect(); // Each rules item has the same length. Each item is 32 bits = 4 bytes. out.total_size = out.trie.total_size + out.rules_gc.len() * out.rules_gc[0].len() * 4; if out.arg_line_breaks { out.total_size += out.rules_lb.len() * 4; } // Run a quick sanity check to ensure that the trie works as expected. for (cp, expected) in out.ucd.values.iter().enumerate() { let mut actual = 0; for s in &out.trie.stages { actual = s.values[actual as usize + ((cp >> s.shift) & s.mask)]; } assert_eq!(expected.value(), actual, "trie sanity check failed for U+{cp:04X}"); } for (cp, &expected) in out.ucd.values[..0x80].iter().enumerate() { let last = out.trie.stages.last().unwrap(); let actual = last.values[cp]; assert_eq!( expected.value(), actual, "trie sanity check failed for direct ASCII mapping of U+{cp:04X}" ); } let buf = match out.arg_lang { Language::C => generate_c(out), Language::Rust => generate_rust(out), }; std::io::stdout().write_all(buf.as_bytes())?; Ok(()) } fn generate_c(out: Output) -> String { let mut buf = String::new(); _ = writedoc!( buf, " // BEGIN: Generated by grapheme-table-gen on {}, from {}, with {}, {} bytes // clang-format off ", chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true), out.ucd.description, out.args(), out.total_size, ); for stage in &out.trie.stages { let mut width = 16; if stage.index != 0 { width = stage.mask + 1; } _ = write!(buf, "static const uint{}_t s_stage{}[] = {{", stage.bits, stage.index); for (j, &value) in stage.values.iter().enumerate() { if j % width == 0 { buf.push_str("\n "); } _ = write!(buf, " 0x{:01$x},", value, stage.bits / 4); } buf.push_str("\n};\n"); } _ = writeln!( buf, "static const uint32_t s_grapheme_cluster_join_rules[{}][{}] = {{", out.rules_gc.len(), out.rules_gc[0].len() ); for table in &out.rules_gc { buf.push_str(" {\n"); for &r in table { _ = writeln!(buf, " 0b{r:032b},"); } buf.push_str(" },\n"); } buf.push_str("};\n"); if out.arg_line_breaks { _ = writeln!( buf, "static const uint32_t s_line_break_join_rules[{}] = {{", out.rules_lb.len() ); for r in &out.rules_lb { _ = writeln!(buf, " 0b{r:032b},"); } buf.push_str("};\n"); } _ = writedoc!( buf, " inline int ucd_grapheme_cluster_lookup(const uint32_t cp) {{ if (cp < 0x80) {{ return s_stage{}[cp]; }} ", out.trie.stages.len() - 1, ); for stage in &out.trie.stages { if stage.index == 0 { _ = writeln!( buf, " const uint{}_t s0 = s_stage0[cp >> {}];", stage.bits, stage.shift, ); } else { _ = writeln!( buf, " const uint{}_t s{} = s_stage{}[s{} + ((cp >> {}) & {})];", stage.bits, stage.index, stage.index, stage.index - 1, stage.shift, stage.mask, ); } } _ = writedoc!( buf, " return s{}; }} ", out.trie.stages.len() - 1, ); _ = writedoc!( buf, " inline int ucd_grapheme_cluster_joins(const int state, const int lead, const int trail) {{ const int l = lead & {0}; const int t = trail & {0}; return (s_grapheme_cluster_join_rules[state][l] >> (t * 2)) & 3; }} inline bool ucd_grapheme_cluster_joins_done(const int state) {{ return state == 3; }} ", out.ucd.packing.mask_cluster_break, ); if out.arg_no_ambiguous { _ = writedoc!( buf, " inline int ucd_grapheme_cluster_character_width(const int val) {{ return val >> {}; }} ", out.ucd.packing.shift_character_width, ); } else { _ = writedoc!( buf, " inline int ucd_grapheme_cluster_character_width(const int val, int ambiguous_width) {{ int w = val >> {}; if (w == 3) {{ w = ambiguous_width; }} return w; }} ", out.ucd.packing.shift_character_width, ); } if out.arg_line_breaks { _ = writedoc!( buf, " inline bool ucd_line_break_joins(const int lead, const int trail) {{ const int l = (lead >> {0}) & {1}; const int t = (trail >> {0}) & {1}; return (s_line_break_join_rules[l] >> t) & 1; }} ", out.ucd.packing.shift_line_break, out.ucd.packing.mask_line_break, ); } if out.arg_extended { _ = writedoc!( buf, " inline int ucd_start_of_text_properties() {{ return {:#x}; }} inline int ucd_tab_properties() {{ return {:#x}; }} inline int ucd_linefeed_properties() {{ return {:#x}; }} ", TrieType::new( &out.ucd.packing, // Control behaves identical to SOT (start of text) in a way, // as it doesn't join with any surrounding character. ClusterBreak::Control, LineBreak::StartOfText, CharacterWidth::ZeroWidth, ) .value(), out.ucd.values['\t' as usize].value(), out.ucd.values['\n' as usize].value(), ); } buf.push_str("// clang-format on\n// END: Generated by grapheme-table-gen\n"); buf } fn generate_rust(out: Output) -> String { let mut buf = String::new(); _ = writeln!( buf, "// BEGIN: Generated by grapheme-table-gen on {}, from {}, with {}, {} bytes", chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true), out.ucd.description, out.args(), out.total_size, ); for stage in &out.trie.stages { let mut width = 16; if stage.index != 0 { width = stage.mask + 1; } _ = write!( buf, "#[rustfmt::skip]\nconst STAGE{}: [u{}; {}] = [", stage.index, stage.bits, stage.values.len(), ); for (j, &value) in stage.values.iter().enumerate() { if j % width == 0 { buf.push_str("\n "); } _ = write!(buf, " 0x{:01$x},", value, stage.bits / 4); } buf.push_str("\n];\n"); } _ = writeln!( buf, "#[rustfmt::skip]\nconst GRAPHEME_JOIN_RULES: [[u32; {}]; {}] = [", out.rules_gc[0].len(), out.rules_gc.len(), ); for table in &out.rules_gc { buf.push_str(" [\n"); for &r in table { _ = writeln!(buf, " 0b{r:032b},"); } buf.push_str(" ],\n"); } buf.push_str("];\n"); if out.arg_line_breaks { _ = writeln!( buf, "#[rustfmt::skip]\nconst LINE_BREAK_JOIN_RULES: [u32; {}] = [", out.rules_lb.len(), ); for r in &out.rules_lb { _ = writeln!(buf, " 0b{r:032b},"); } buf.push_str("];\n"); } _ = writedoc!( buf, " #[inline(always)] pub fn ucd_grapheme_cluster_lookup(cp: char) -> usize {{ unsafe {{ let cp = cp as usize; if cp < 0x80 {{ return STAGE{}[cp] as usize; }} ", out.trie.stages.len() - 1, ); for stage in &out.trie.stages { if stage.index == 0 { _ = writeln!( buf, " let s = *STAGE{}.get_unchecked(cp >> {}) as usize;", stage.index, stage.shift, ); } else if stage.index != out.trie.stages.len() - 1 { _ = writeln!( buf, " let s = *STAGE{}.get_unchecked(s + ((cp >> {}) & {})) as usize;", stage.index, stage.shift, stage.mask, ); } else { _ = writeln!( buf, " *STAGE{}.get_unchecked(s + (cp & {})) as usize", stage.index, stage.mask, ); } } _ = writedoc!( buf, " }} }} ", ); _ = writedoc!( buf, " #[inline(always)] pub fn ucd_grapheme_cluster_joins(state: u32, lead: usize, trail: usize) -> u32 {{ unsafe {{ let l = lead & {0}; let t = trail & {0}; let s = GRAPHEME_JOIN_RULES.get_unchecked(state as usize); (s[l] >> (t * 2)) & 3 }} }} #[inline(always)] pub fn ucd_grapheme_cluster_joins_done(state: u32) -> bool {{ state == 3 }} ", out.ucd.packing.mask_cluster_break, ); if out.arg_no_ambiguous { _ = writedoc!( buf, " #[inline(always)] pub fn ucd_grapheme_cluster_character_width(val: usize) -> usize {{ val >> {} }} ", out.ucd.packing.shift_character_width, ); } else { // `cold_path()` ensures that LLVM emits a branch instead of a conditional move. // This improves performance, as ambiguous characters are rare. // `> 2` is used instead of `== 3`, because this way the compiler can immediately // test whether `val > (2 << shift_character_width)` before shifting. _ = writedoc!( buf, " #[inline(always)] pub fn ucd_grapheme_cluster_character_width(val: usize, ambiguous_width: usize) -> usize {{ let mut w = val >> {}; if w > 2 {{ cold_path(); w = ambiguous_width; }} w }} ", out.ucd.packing.shift_character_width, ); } if out.arg_line_breaks { _ = writedoc!( buf, " #[inline(always)] pub fn ucd_line_break_joins(lead: usize, trail: usize) -> bool {{ unsafe {{ let l = (lead >> {0}) & {1}; let t = (trail >> {0}) & {1}; let s = *LINE_BREAK_JOIN_RULES.get_unchecked(l); ((s >> t) & 1) != 0 }} }} ", out.ucd.packing.shift_line_break, out.ucd.packing.mask_line_break, ); } if out.arg_extended { _ = writedoc!( buf, " #[inline(always)] pub fn ucd_start_of_text_properties() -> usize {{ {:#x} }} #[inline(always)] pub fn ucd_tab_properties() -> usize {{ {:#x} }} #[inline(always)] pub fn ucd_linefeed_properties() -> usize {{ {:#x} }} ", TrieType::new( &out.ucd.packing, // Control behaves identical to SOT (start of text) in a way, // as it doesn't join with any surrounding character. ClusterBreak::Control, LineBreak::StartOfText, CharacterWidth::ZeroWidth, ) .value(), out.ucd.values['\t' as usize].value(), out.ucd.values['\n' as usize].value(), ); } if !out.arg_no_ambiguous { _ = writedoc!( buf, " #[cold] #[inline(always)] fn cold_path() {{}} " ); } buf.push_str("// END: Generated by grapheme-table-gen\n"); buf } fn extract_values_from_ucd(doc: &roxmltree::Document, out: &Output) -> anyhow::Result { let packing = BitPacking::new(out.arg_line_breaks, out.arg_extended); let ambiguous_value = if out.arg_no_ambiguous { CharacterWidth::Narrow } else { CharacterWidth::Ambiguous }; let mut values = vec![ TrieType::new(&packing, ClusterBreak::Other, LineBreak::Other, CharacterWidth::Narrow,); 1114112 ]; let ns = "http://www.unicode.org/ns/2003/ucd/1.0"; let root = doc.root_element(); let description = root .children() .find(|n| n.has_tag_name((ns, "description"))) .context("missing ucd description")?; let repertoire = root .children() .find(|n| n.has_tag_name((ns, "repertoire"))) .context("missing ucd repertoire")?; let description = description.text().unwrap_or_default().to_string(); for group in repertoire.children().filter(|n| n.is_element()) { const DEFAULT_ATTRIBUTES: UcdAttributes = UcdAttributes { general_category: "", line_break: "", grapheme_cluster_break: "", indic_conjunct_break: "", extended_pictographic: "", east_asian: "", }; let group_attributes = extract_attributes(&group, &DEFAULT_ATTRIBUTES); for char in group.children().filter(|n| n.is_element()) { let char_attributes = extract_attributes(&char, &group_attributes); let range = extract_range(&char); let mut cb = match char_attributes.grapheme_cluster_break { "XX" => ClusterBreak::Other, // Anything else // We ignore GB3 which demands that CR × LF do not break apart, because // * these control characters won't normally reach our text storage // * otherwise we're in a raw write mode and historically conhost stores them in separate cells "CR" => ClusterBreak::CR, // Carriage Return "LF" => ClusterBreak::LF, // Line Feed "CN" => ClusterBreak::Control, // Control "EX" | "SM" => ClusterBreak::Extend, // Extend, SpacingMark "PP" => ClusterBreak::Prepend, // Prepend "ZWJ" => ClusterBreak::ZWJ, // Zero Width Joiner "RI" => ClusterBreak::RI, // Regional Indicator "L" => ClusterBreak::HangulL, // Hangul Syllable Type L "V" => ClusterBreak::HangulV, // Hangul Syllable Type V "T" => ClusterBreak::HangulT, // Hangul Syllable Type T "LV" => ClusterBreak::HangulLV, // Hangul Syllable Type LV "LVT" => ClusterBreak::HangulLVT, // Hangul Syllable Type LVT _ => bail!( "Unrecognized GCB={} for U+{:04X} to U+{:04X}", char_attributes.grapheme_cluster_break, range.start(), range.end() ), }; if char_attributes.extended_pictographic == "Y" { // Currently every single Extended_Pictographic codepoint happens to be GCB=XX. // This is fantastic for us because it means we can stuff it into the ClusterBreak enum // and treat it as an alias of EXTEND, but with the special GB11 properties. if cb != ClusterBreak::Other { bail!( "Unexpected GCB={} with ExtPict=Y for U+{:04X} to U+{:04X}", char_attributes.grapheme_cluster_break, range.start(), range.end() ); } cb = ClusterBreak::ExtPic; } if !matches!(char_attributes.indic_conjunct_break, "None" | "Extend") { // If it's not None/Extend, it's Linker/Consonant, and currently // all of them are GCB=EX/XX. Since we treat them almost like extenders, // we need to revisit our assumptions if this ever changes. if !matches!(cb, ClusterBreak::Other | ClusterBreak::Extend) { bail!( "Unexpected GCB={} with InCB={} for U+{:04X} to U+{:04X}", char_attributes.grapheme_cluster_break, char_attributes.indic_conjunct_break, range.start(), range.end() ); } cb = match char_attributes.indic_conjunct_break { "Linker" => ClusterBreak::InCBLinker, "Consonant" => ClusterBreak::InCBConsonant, _ => bail!( "Unrecognized InCB={} for U+{:04X} to U+{:04X}", char_attributes.indic_conjunct_break, range.start(), range.end() ), }; } let mut cw = match char_attributes.east_asian { "N" | "Na" | "H" => CharacterWidth::Narrow, // Half-width, Narrow, Neutral "F" | "W" => CharacterWidth::Wide, // Wide, Full-width "A" => ambiguous_value, // Ambiguous _ => bail!( "Unrecognized ea={} for U+{:04X} to U+{:04X}", char_attributes.east_asian, range.start(), range.end() ), }; // There's no "ea" attribute for "zero width" so we need to do that ourselves. This matches: // Me: Mark, enclosing // Mn: Mark, non-spacing // Cf: Control, format match char_attributes.general_category { "Cf" if cb == ClusterBreak::Control => { // A significant portion of Cf characters are not just gc=Cf (= commonly considered zero-width), // but also GCB=CN (= does not join). This is a bit of a problem for terminals, // because they don't support zero-width graphemes, as zero-width columns can't exist. // So, we turn all of them into Extend, which is roughly how wcswidth() would treat them. cb = ClusterBreak::Extend; cw = CharacterWidth::ZeroWidth; } "Me" | "Mn" | "Cf" => { cw = CharacterWidth::ZeroWidth; } _ => {} }; let lb = if out.arg_line_breaks { let lb_ea = matches!(char_attributes.east_asian, "F" | "W" | "H"); match char_attributes.line_break { "WJ" => LineBreak::WordJoiner, "ZW" => LineBreak::ZeroWidthSpace, "GL" => LineBreak::Glue, "SP" => LineBreak::Space, "BA" => LineBreak::BreakAfter, "BB" => LineBreak::BreakBefore, "HY" => LineBreak::Hyphen, "CL" => LineBreak::ClosePunctuation, "CP" if lb_ea => LineBreak::CloseParenthesis_EA, "CP" => LineBreak::CloseParenthesis_NotEA, "EX" => LineBreak::Exclamation, "IN" => LineBreak::Inseparable, "NS" => LineBreak::Nonstarter, "OP" if lb_ea => LineBreak::OpenPunctuation_EA, "OP" => LineBreak::OpenPunctuation_NotEA, "QU" => LineBreak::Quotation, "IS" => LineBreak::InfixNumericSeparator, "NU" => LineBreak::Numeric, "PO" => LineBreak::PostfixNumeric, "PR" => LineBreak::PrefixNumeric, "SY" => LineBreak::SymbolsAllowingBreakAfter, "AL" | "HL" => LineBreak::Alphabetic, "ID" | "EB" | "EM" => LineBreak::Ideographic, _ => LineBreak::Other, } } else { LineBreak::Other }; values[range].fill(TrieType::new(&packing, cb, lb, cw)); } } if out.arg_extended { values['\t' as usize].change_cluster_break_ext(&packing, ClusterBreakExt::ControlTab); } // U+00AD: Soft Hyphen // A soft hyphen is a hint that a word break is allowed at that position. // By default, the glyph is supposed to be invisible, and only if // a word break occurs, the text renderer should display a hyphen. // A terminal does not support computerized typesetting, but unlike the other // gc=Cf cases we give it a Narrow width, because that matches wcswidth(). values[0x00AD].change_width(&packing, CharacterWidth::Narrow); // U+2500 to U+257F: Box Drawing block // U+2580 to U+259F: Block Elements block // By default, CharacterWidth.Ambiguous, but by convention .Narrow in terminals. // // Most of these characters are LineBreak.Other, but some are actually LineBreak.Alphabetic. // But to us this doesn't really matter much, because it doesn't make much sense anyway that // a light double dash is "alphabetic" while a light triple dash is not. values[0x2500..=0x259F].fill(TrieType::new( &packing, ClusterBreak::Other, LineBreak::Other, CharacterWidth::Narrow, )); // U+FE0F Variation Selector-16 is used to turn unqualified Emojis into qualified ones. // By convention, this turns them from being ambiguous width (= narrow) into wide ones. // We achieve this here by explicitly giving this codepoint a wide width. // Later down below we'll clamp width back to <= 2. // // U+FE0F actually has a LineBreak property of CM (Combining Mark), // but for us that's equivalent to Other. values[0xFE0F].change_width(&packing, CharacterWidth::Wide); Ok(Ucd { description, values, packing }) } struct UcdAttributes<'a> { general_category: &'a str, line_break: &'a str, grapheme_cluster_break: &'a str, indic_conjunct_break: &'a str, extended_pictographic: &'a str, east_asian: &'a str, } fn extract_attributes<'a>( node: &'a roxmltree::Node, default: &'a UcdAttributes, ) -> UcdAttributes<'a> { UcdAttributes { general_category: node.attribute("gc").unwrap_or(default.general_category), line_break: node.attribute("lb").unwrap_or(default.line_break), grapheme_cluster_break: node.attribute("GCB").unwrap_or(default.grapheme_cluster_break), indic_conjunct_break: node.attribute("InCB").unwrap_or(default.indic_conjunct_break), extended_pictographic: node.attribute("ExtPict").unwrap_or(default.extended_pictographic), east_asian: node.attribute("ea").unwrap_or(default.east_asian), } } fn extract_range(node: &roxmltree::Node) -> RangeInclusive { let (first, last) = match node.attribute("cp") { Some(val) => { let cp = usize::from_str_radix(val, 16).unwrap(); (cp, cp) } None => ( usize::from_str_radix(node.attribute("first-cp").unwrap_or("0"), 16).unwrap(), usize::from_str_radix(node.attribute("last-cp").unwrap_or("0"), 16).unwrap(), ), }; first..=last } fn build_best_trie( uncompressed: &[TrieType], min_shift: usize, max_shift: usize, stages: usize, ) -> Trie { let depth = stages - 1; let delta = max_shift - min_shift + 1; let total = delta.pow(depth as u32); let mut tasks = Vec::new(); for i in 0..total { let mut shifts = vec![0; depth]; let mut index = i; for s in &mut shifts { *s = min_shift + (index % delta); index /= delta; } tasks.push(shifts); } tasks .par_iter() .map(|shifts| build_trie(uncompressed.to_vec(), shifts)) .min_by_key(|t| t.total_size) .unwrap() } fn build_trie(uncompressed: Vec, shifts: &[usize]) -> Trie { // Fun fact: Rust optimizes the into_iter/collect into a no-op. Neat! let mut uncompressed: Vec = uncompressed.into_iter().map(|c| c.value()).collect(); let mut cumulative_shift = 0; let mut stages = Vec::new(); for (stage, &shift) in shifts.iter().enumerate() { let chunk_size = 1 << shift; let mut cache = HashMap::new(); let mut compressed = Vec::new(); let mut offsets = Vec::new(); let mut off = 0; while off < uncompressed.len() { let chunk = &uncompressed[off..off + chunk_size.min(uncompressed.len() - off)]; let offset = if stage == 0 && off < 0x80 { // The first stage (well, really the last stage - the one which contains the values instead of indices) // contains a direct 1:1 mapping for all ASCII codepoints as they're most common in IT environments. compressed.extend_from_slice(chunk); (compressed.len() - chunk.len()) as u32 } else { *cache.entry(chunk).or_insert_with(|| { if let Some(existing) = find_existing(&compressed, chunk) { existing as u32 } else { let overlap = measure_overlap(&compressed, chunk); compressed.extend_from_slice(&chunk[overlap..]); (compressed.len() - chunk.len()) as u32 } }) }; offsets.push(offset); off += chunk.len(); } stages.push(Stage { values: compressed, index: shifts.len() - stages.len(), shift: cumulative_shift, mask: chunk_size - 1, bits: 0, }); uncompressed = offsets; cumulative_shift += shift; } stages.push(Stage { values: uncompressed, index: 0, shift: cumulative_shift, mask: usize::MAX, bits: 0, }); stages.reverse(); for stage in stages.iter_mut() { let max_val = stage.values.iter().max().cloned().unwrap_or(0); stage.bits = match max_val { 0..0x100 => 8, 0x100..0x10000 => 16, _ => 32, }; } let total_size: usize = stages.iter().map(|stage| (stage.bits / 8) * stage.values.len()).sum(); Trie { stages, total_size } } fn find_existing(haystack: &[u32], needle: &[u32]) -> Option { haystack.windows(needle.len()).position(|window| window == needle) } fn measure_overlap(prev: &[u32], next: &[u32]) -> usize { (0..prev.len().min(next.len())) .rev() .find(|&i| prev[prev.len() - i..] == next[..i]) .unwrap_or(0) } fn prepare_rules_row(row: &[i32], bit_width: usize, non_joiner_value: i32) -> u32 { row.iter().enumerate().fold(0u32, |acc, (trail, &value)| { let value = if value < 0 { non_joiner_value } else { value }; acc | ((value as u32) << (trail * bit_width)) }) } ================================================ FILE: crates/unicode-gen/src/rules.rs ================================================ // 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 /* | */], ]; ================================================ FILE: i18n/edit.toml ================================================ __default__ = [ "en", # -------- "de", "es", "fr", "it", "ja", "ko", "pt_br", "ru", "zh_hans", "zh_hant", ] [__alias__] zh = "zh_hans" # The keyboard key [Ctrl] en = "Ctrl" de = "Strg" # The keyboard key [Alt] en = "Alt" # The keyboard key [Shift] en = "Shift" da = "Skift" de = "Umschalt" es = "Mayús" fi = "Vaihto" fr = "Maj" it = "Maiusc" # Used as a common dialog button [Ok] en = "Ok" ar = "حسناً" bn = "ঠিক আছে" cs = "Ok" da = "OK" de = "OK" el = "ΟΚ" es = "Aceptar" fa = "پذیرفتن" fi = "OK" fr = "OK" hu = "OK" id = "OK" is = "Í lagi" it = "OK" ja = "OK" ko = "확인" nl = "OK" pt_br = "OK" pt_pt = "OK" ro = "OK" ru = "ОК" sr = "U redu" sv = "OK" tk = "Bolýar" tr = "Tamam" vi = "OK" zh_hans = "确定" zh_hant = "確定" # Used as a common dialog button [Yes] en = "Yes" ar = "نعم" bn = "হ্যাঁ" cs = "Ano" da = "Ja" de = "Ja" ee = "Jah" el = "Ναι" es = "Sí" fa = "بله" fi = "Kyllä" fr = "Oui" hu = "Igen" id = "Ya" is = "Já" it = "Sì" ja = "はい" ko = "예" nl = "Ja" pl = "Tak" pt_br = "Sim" pt_pt = "Sim" ro = "Da" ru = "Да" sr = "Da" sv = "Ja" tk = "Hawa" tr = "Evet" uk = "Так" vi = "Có" zh_hans = "是" zh_hant = "是" # Used as a common dialog button [No] en = "No" ar = "لا" bn = "না" cs = "Ne" da = "Nej" de = "Nein" ee = "Ei" el = "Όχι" es = "No" fa = "خیر" fi = "Ei" fr = "Non" hu = "Nem" id = "Tidak" is = "Nei" it = "No" ja = "いいえ" ko = "아니요" nl = "Nee" pl = "Nie" pt_br = "Não" pt_pt = "Não" ro = "Nu" ru = "Нет" sr = "Ne" sv = "Nej" tk = "Ýok" tr = "Hayır" uk = "Ні" vi = "Không" zh_hans = "否" zh_hant = "否" # Used as a common dialog button [Cancel] en = "Cancel" ar = "إلغاء" bn = "বাতিল" cs = "Zrušit" da = "Annuller" de = "Abbrechen" ee = "Tühista" el = "Άκυρο" es = "Cancelar" fa = "لغو" fi = "Peruuta" fr = "Annuler" hu = "Mégse" id = "Batal" is = "Hætta við" it = "Annulla" ja = "キャンセル" ko = "취소" nl = "Annuleer" pl = "Anuluj" pt_br = "Cancelar" pt_pt = "Cancelar" ro = "Anulare" ru = "Отмена" sr = "Otkaži" sv = "Avbryt" tk = "Ýatyr" tr = "İptal" uk = "Скасувати" vi = "Hủy" zh_hans = "取消" zh_hant = "取消" # Used as a common dialog button [Always] en = "Always" ar = "دائماً" bn = "সবসময়" cs = "Vždy" da = "Altid" de = "Immer" ee = "Alati" el = "Πάντα" es = "Siempre" fa = "همیشه" fi = "Aina" fr = "Toujours" hu = "Mindig" id = "Selalu" is = "Alltaf" it = "Sempre" ja = "常に" ko = "항상" nl = "Altijd" pl = "Zawsze" pt_br = "Sempre" pt_pt = "Sempre" ro = "Întotdeauna" ru = "Всегда" sr = "Uvek" sv = "Alltid" tr = "Her Zaman" uk = "Завжди" vi = "Luôn" zh_hans = "总是" zh_hant = "總是" # A menu bar item [File] en = "File" ar = "ملف" bn = "ফাইল" cs = "Soubor" da = "Filer" de = "Datei" ee = "Fail" el = "Αρχείο" es = "Archivo" fa = "پرونده" fi = "Tiedosto" fr = "Fichier" hu = "Fájl" is = "Skrá" it = "File" ja = "ファイル" ko = "파일" nl = "Bestand" pl = "Plik" pt_br = "Arquivo" pt_pt = "Ficheiro" ro = "Fișier" ru = "Файл" sr = "Fajl" sv = "Fil" tk = "Faýl" tr = "Dosya" uk = "Файл" vi = "Tệp" zh_hans = "文件" zh_hant = "檔案" [FileNew] en = "New File" ar = "ملف جديد" bn = "নতুন ফাইল…" cs = "Nový soubor" da = "Ny fil…" de = "Neue Datei" ee = "Uus fail" el = "Νέο αρχείο" es = "Nuevo archivo" fa = "پروندهٔ نو…" fi = "Uusi tiedosto" fr = "Nouveau fichier" hu = "Új fájl" id = "File Baru" is = "Ný skrá…" it = "Nuovo file" ja = "新規ファイル" ko = "새 파일" nl = "Nieuw Bestand" pl = "Nowy plik" pt_br = "Novo Arquivo" pt_pt = "Novo ficheiro" ro = "Fișier nou" ru = "Новый файл" sr = "Novi fajl…" sv = "Ny fil…" tk = "Täze faýl…" tr = "Yeni Dosya" uk = "Новий файл" vi = "Tệp mới" zh_hans = "新建文件" zh_hant = "新增檔案" [FileOpen] en = "Open File…" ar = "فتح ملف…" bn = "ফাইল খুলুন…" cs = "Otevřít soubor…" da = "Åbn fil…" de = "Datei öffnen…" ee = "Ava fail…" el = "Άνοιγμα αρχείου…" es = "Abrir archivo…" fa = "باز کردن پرونده…" fi = "Avaa tiedosto…" fr = "Ouvrir un fichier…" hu = "Fájl megnyitása…" id = "Buka File…" is = "Opna skrá…" it = "Apri file…" ja = "ファイルを開く…" ko = "파일 열기…" nl = "Open bestand…" pl = "Otwórz plik…" pt_br = "Abrir o Arquivo…" pt_pt = "Abrir ficheiro…" ro = "Deschide fișier…" ru = "Открыть файл…" sr = "Otvori fajl…" sv = "Öppna fil…" tk = "Faýly aç…" tr = "Dosya Aç…" uk = "Відкрити файл…" vi = "Mở tệp…" zh_hans = "打开文件…" zh_hant = "開啟檔案…" [FileSave] en = "Save" ar = "حفظ" bn = "সংরক্ষণ" cs = "Uložit" da = "Gem" de = "Speichern" ee = "Salvesta" el = "Αποθήκευση" es = "Guardar" fa = "ذخیره کردن" fi = "Tallenna" fr = "Enregistrer" hu = "Mentés" id = "Simpan" is = "Vista" it = "Salva" ja = "保存" ko = "저장" nl = "Opslaan" pl = "Zapisz" pt_br = "Salvar" pt_pt = "Guardar" ro = "Salvează" ru = "Сохранить" sr = "Sačuvaj" sv = "Spara" tk = "Sakla" tr = "Kaydet" uk = "Зберегти" vi = "Lưu" zh_hans = "保存" zh_hant = "儲存" [FileSaveAs] en = "Save As…" ar = "حفظ باسم…" bn = "অন্য নামে সংরক্ষণ…" cs = "Uložit jako…" da = "Gem som…" de = "Speichern unter…" ee = "Salvesta kui…" el = "Αποθήκευση ως…" es = "Guardar como…" fa = "ذخیره کردن به‌عنوان…" fi = "Tallenna nimellä…" fr = "Enregistrer sous…" hu = "Mentés másként…" id = "Simpan Sebagai…" is = "Vista sem…" it = "Salva come…" ja = "名前を付けて保存…" ko = "다른 이름으로 저장…" nl = "Opslaan Als…" pl = "Zapisz jako…" pt_br = "Salvar Como…" pt_pt = "Guardar como…" ro = "Salvează ca…" ru = "Сохранить как…" sr = "Sačuvaj kao…" sv = "Spara som…" tk = "Başga görnüşde sakla…" tr = "Farklı Kaydet…" uk = "Зберегти як…" vi = "Lưu thành…" zh_hans = "另存为…" zh_hant = "另存新檔…" [FileClose] en = "Close File" ar = "إغلاق الملف" bn = "এডিটর বন্ধ করুন" cs = "Zavřít editor" da = "Luk editor" de = "Datei schließen" ee = "Sulge fail" el = "Κλείσιμο επεξεργαστή" es = "Cerrar archivo" fa = "بستن ویراستار" fi = "Sulje tiedosto" fr = "Fermer le fichier" hu = "Fájl bezárása" id = "Tutup File" is = "Loka ritli" it = "Chiudi file" ja = "ファイルを閉じる" ko = "파일 닫기" nl = "Sluit Bestand" pl = "Zamknij plik" pt_br = "Fechar arquivo" pt_pt = "Fechar ficheiro" ro = "Închide fișier" ru = "Закрыть файл" sr = "Zatvori uređivač" sv = "Stäng editor" tk = "Düzeldijini ýap" tr = "Dosyayı Kapat" uk = "Закрити файл" vi = "Đóng tệp" zh_hans = "关闭文件" zh_hant = "關閉檔案" [FileExit] en = "Exit" ar = "خروج" bn = "প্রস্থান" cs = "Ukončit" da = "Afslut" de = "Beenden" ee = "Välju" el = "Έξοδος" es = "Salir" fa = "خروج" fi = "Lopeta" fr = "Quitter" hu = "Kilépés" id = "Keluar" is = "Hætta" it = "Esci" ja = "終了" ko = "종료" nl = "Afsluiten" pl = "Zakończ" pt_br = "Sair" pt_pt = "Sair" ro = "Ieșire" ru = "Выход" sr = "Izlaz" sv = "Avsluta" tk = "Çyk" tr = "Çıkış" uk = "Вихід" vi = "Thoát" zh_hans = "退出" zh_hant = "退出" [FileGoto] en = "Go to Line:Column…" ar = "الانتقال إلى السطر:العمود…" cs = "Přejít na řádek/sloupec…" de = "Gehe zu Zeile:Spalte…" ee = "Mine reale:veergu…" el = "Μετάβαση σε γραμμή:στήλη…" es = "Ir a línea:columna…" fi = "Siirry kohtaan rivi:sarake" fr = "Aller à la ligne:colonne…" hu = "Ugrás ide sor:oszlop…" id = "Pergi ke Baris:Kolom…" is = "Fara í línu/dálk…" it = "Vai a riga:colonna…" ja = "行:列へ移動…" ko = "줄:열로 이동…" nl = "Ga naar Regel:Kolom…" pl = "Idź do wiersza:kolumny…" pt_br = "Ir para a Linha:Coluna…" pt_pt = "Ir para a linha:Coluna…" ro = "Mergi la linie:coloană…" ru = "Перейти к строке:столбцу…" tk = "Setire / sütüne geç…" tr = "Satıra:Sütuna Git…" uk = "Перейти на рядок:стовпчик…" vi = "Đi tới dòng:cột…" zh_hans = "转到行:列…" zh_hant = "跳至行:列…" # A menu bar item [Edit] en = "Edit" ar = "تعديل" bn = "সম্পাদনা" cs = "Úpravy" da = "Rediger" de = "Bearbeiten" ee = "Muuda" el = "Επεξεργασία" es = "Editar" fa = "ویرایش" fi = "Muokkaa" fr = "Édition" hu = "Szerkesztés" is = "Breyta" it = "Modifica" ja = "編集" ko = "편집" nl = "Bewerken" pl = "Edycja" pt_br = "Editar" pt_pt = "Editar" ro = "Editare" ru = "Правка" sr = "Uredi" sv = "Redigera" tk = "Düzelt" tr = "Düzen" uk = "Редагувати" vi = "Chỉnh sửa" zh_hans = "编辑" zh_hant = "編輯" [EditUndo] en = "Undo" ar = "تراجع" bn = "পূর্বাবস্থায় ফেরান" cs = "Zpět" da = "Fortryd" de = "Rückgängig" ee = "Võta tagasi" el = "Αναίρεση" es = "Deshacer" fa = "واگرد" fi = "Kumoa" fr = "Annuler" hu = "Visszavonás" id = "Batalkan" is = "Afturkalla" it = "Annulla" ja = "元に戻す" ko = "실행 취소" nl = "Ongedaan Maken" pl = "Cofnij" pt_br = "Desfazer" pt_pt = "Anular" ro = "Anulează" ru = "Отменить" sr = "Poništi" sv = "Ångra" tk = "Yza gaýtar" tr = "Geri Al" uk = "Скасувати" vi = "Hoàn tác" zh_hans = "撤销" zh_hant = "復原" [EditRedo] en = "Redo" ar = "إعادة" bn = "পুনরায় করুন" cs = "Znovu" da = "Gentag" de = "Wiederholen" ee = "Tee uuesti" el = "Επανάληψη" es = "Rehacer" fa = "از نو" fi = "Tee uudelleen" fr = "Rétablir" hu = "Újra" id = "Ulangi" is = "Endurtaka" it = "Ripeti" ja = "やり直し" ko = "다시 실행" nl = "Opnieuw Uitvoeren" pl = "Ponów" pt_br = "Refazer" pt_pt = "Refazer" ro = "Refă" ru = "Повторить" sr = "Vrati" sv = "Gör om" tk = "Gaýtadan işle" tr = "Yinele" uk = "Повторити" vi = "Làm lại" zh_hans = "重做" zh_hant = "重做" [EditCut] en = "Cut" ar = "قص" bn = "কাট" cs = "Vyjmout" da = "Klip" de = "Ausschneiden" ee = "Lõika" el = "Αποκοπή" es = "Cortar" fa = "برش" fi = "Leikkaa" fr = "Couper" hu = "Kivágás" id = "Potong" is = "Klippa" it = "Taglia" ja = "切り取り" ko = "잘라내기" nl = "Knippen" pl = "Wytnij" pt_br = "Recortar" pt_pt = "Cortar" ro = "Decupează" ru = "Вырезать" sr = "Iseci" sv = "Klipp ut" tk = "Kes" tr = "Kes" uk = "Вирізати" vi = "Cắt" zh_hans = "剪切" zh_hant = "剪下" [EditCopy] en = "Copy" ar = "نسخ" bn = "কপি" cs = "Kopírovat" da = "Kopier" de = "Kopieren" ee = "Kopeeri" el = "Αντιγραφή" es = "Copiar" fa = "رونوشت" fi = "Kopioi" fr = "Copier" hu = "Másolás" id = "Salin" is = "Afrita" it = "Copia" ja = "コピー" ko = "복사" nl = "Kopiëren" pl = "Kopiuj" pt_br = "Copiar" pt_pt = "Copiar" ro = "Copiază" ru = "Копировать" sr = "Kopiraj" sv = "Kopiera" tk = "Göçür" tr = "Kopyala" uk = "Копіювати" vi = "Sao chép" zh_hans = "复制" zh_hant = "複製" [EditPaste] en = "Paste" ar = "لصق" bn = "পেস্ট" cs = "Vložit" da = "Sæt ind" de = "Einfügen" ee = "Kleebi" el = "Επικόλληση" es = "Pegar" fa = "چسپاندن" fi = "Liitä" fr = "Coller" hu = "Beillesztés" id = "Tempel" is = "Líma" it = "Incolla" ja = "貼り付け" ko = "붙여넣기" nl = "Plakken" pl = "Wklej" pt_br = "Colar" pt_pt = "Colar" ro = "Lipește" ru = "Вставить" sr = "Nalepi" sv = "Klistra in" tk = "Ýelme" tr = "Yapıştır" uk = "Вставити" vi = "Dán" zh_hans = "粘贴" zh_hant = "貼上" [EditFind] en = "Find" ar = "بحث" bn = "অনুসন্ধান" cs = "Najít" da = "Find" de = "Suchen" ee = "Otsi" el = "Εύρεση" es = "Buscar" fa = "یافتن" fi = "Etsi" fr = "Rechercher" hu = "Keresés" id = "Temukan" is = "Finna" it = "Trova" ja = "検索" ko = "찾기" nl = "Zoeken" pl = "Znajdź" pt_br = "Localizar" pt_pt = "Procurar" ro = "Găsește" ru = "Найти" sr = "Pronađi" sv = "Sök" tk = "Gözle" tr = "Bul" uk = "Знайти" vi = "Tìm kiếm" zh_hans = "查找" zh_hant = "尋找" [EditReplace] en = "Replace" ar = "استبدال" bn = "প্রতিস্থাপন" cs = "Nahradit" da = "Erstat" de = "Ersetzen" ee = "Asenda" el = "Αντικατάσταση" es = "Reemplazar" fa = "جایگزینی" fi = "Korvaa" fr = "Remplacer" hu = "Csere" id = "Ganti" is = "Skipta út" it = "Sostituisci" ja = "置換" ko = "바꾸기" nl = "Vervangen" pl = "Zamień" pt_br = "Substituir" pt_pt = "Substituir" ro = "Înlocuiește" ru = "Заменить" sr = "Zameni" sv = "Ersätt" tk = "Çalyş" tr = "Değiştir" uk = "Замінити" vi = "Thay thế" zh_hans = "替换" zh_hant = "取代" [EditSelectAll] en = "Select All" ar = "تحديد الكل" cs = "Vybrat vše" de = "Alles auswählen" ee = "Vali kõik" el = "Επιλογή όλων" es = "Seleccionar todo" fi = "Valitse kaikki" fr = "Tout sélectionner" hu = "Az összes kijelölése" id = "Pilih Semua" is = "Velja allt" it = "Seleziona tutto" ja = "すべて選択" ko = "모두 선택" nl = "Alles Selecteren" pl = "Zaznacz wszystko" pt_br = "Selecionar Tudo" pt_pt = "Selecionar tudo" ro = "Selectează tot" ru = "Выделить всё" tr = "Tümünü Seç" uk = "Вибрати все" vi = "Chọn tất cả" zh_hans = "全选" zh_hant = "全選" # A menu bar item [View] en = "View" ar = "عرض" bn = "দেখুন" cs = "Zobrazit" da = "Vis" de = "Ansicht" ee = "Vaade" el = "Προβολή" es = "Ver" fa = "نمایش" fi = "Näytä" fr = "Affichage" hu = "Nézet" id = "Tampilan" is = "Sýn" it = "Visualizza" ja = "表示" ko = "보기" nl = "Weergave" pl = "Widok" pt_br = "Exibir" pt_pt = "Ver" ro = "Vizualizare" ru = "Вид" sr = "Prikaz" sv = "Visa" tk = "Gör" tr = "Görünüm" uk = "Вигляд" vi = "Xem" zh_hans = "视图" zh_hant = "檢視" [ViewFocusStatusbar] en = "Focus Statusbar" ar = "تركيز شريط الحالة" bn = "স্ট্যাটাসবারে ফোকাস করুন" cs = "Zaměřit na stavový řádek" da = "Fokuser statuslinje" de = "Statusleiste fokussieren" ee = "Fokuseeri staatuseribale" el = "Εστίαση στη γραμμή κατάστασης" es = "Enfocar barra de estado" fa = "کانون به نوار وضعیت" fi = "Fokusoi tilapalkki" fr = "Activer la barre d’état" hu = "Fókusz a státuszsorra" id = "Fokus pada Bar Status" is = "Fókus á stöðuslá" it = "Attiva barra di stato" ja = "ステータスバーにフォーカス" ko = "상태 표시줄로 포커스 이동" nl = "Statusbalk Focussen" pl = "Przejdź na dolny pasek" pt_br = "Focar na Barra de Status" pt_pt = "Focar na barra de estado" ro = "Focalizare pe bara de stare" ru = "Фокус на строку состояния" sr = "Fokusiraj statusnu traku" sv = "Fokusera statusfält" tk = "Ýagdaý setirine üns ber" tr = "Durum Çubuğuna Odaklan" uk = "Фокус на рядок стану" vi = "Đặt tiêu điểm vào thanh trạng thái" zh_hans = "聚焦状态栏" zh_hant = "聚焦狀態列" [ViewWordWrap] en = "Word Wrap" ar = "التفاف النص" bn = "শব্দ মোড়ানো" cs = "Zalamování" da = "Tekstombrydning" de = "Zeilenumbruch" ee = "Murra rida" el = "Αυτόματη επιστροφή σε νέα γραμμή" es = "Ajuste de línea" fa = "سطربندی خطوط" fi = "Automaattinen rivitys" fr = "Retour automatique à la ligne" hu = "Sortörés" id = "Bungkus Kata" is = "Orðskipting" it = "A capo automatico" ja = "折り返し" ko = "자동 줄 바꿈" nl = "Tekstomloop" pl = "Zawijanie wierszy" pt_br = "Quebra De Texto" pt_pt = "Quebra de linha" ro = "Încadrare text" ru = "Перенос слов" sr = "Prelom reči" sv = "Radbrytning" tk = "Söz birikdirme" tr = "Sözcük Kaydır" uk = "Переніс слів" vi = "Ngắt dòng tự động" zh_hans = "自动换行" zh_hant = "自動換行" [ViewGoToFile] en = "Go to File…" ar = "الانتقال إلى ملف…" cs = "Výběr dokumentu…" de = "Gehe zu Datei…" ee = "Mine faili … juurde" es = "Ir a archivo…" fi = "Siirry tiedostoon…" fr = "Aller au fichier…" hu = "Ugrás fájlra…" id = "Pergi ke File…" is = "Fara í skrá…" it = "Vai al file…" ja = "ファイルへ移動…" ko = "파일로 이동…" nl = "Ga naar Bestand" pt_br = "Ir para arquivo…" pt_pt = "Ir para ficheiro…" ro = "Mergi la fișier…" ru = "Перейти к файлу…" tr = "Dosyaya Git…" uk = "Перейти до файлу…" vi = "Đi tới tệp…" zh_hans = "转到文件…" zh_hant = "跳至檔案…" # A menu bar item [Help] en = "Help" ar = "مساعدة" bn = "সাহায্য" cs = "Nápověda" da = "Hjælp" de = "Hilfe" ee = "Abi" el = "Βοήθεια" es = "Ayuda" fa = "کمک" fi = "Ohje" fr = "Aide" hu = "Súgó" id = "Bantuan" is = "Hjálp" it = "Aiuto" ja = "ヘルプ" ko = "도움말" nl = "Help" pl = "Pomoc" pt_br = "Ajuda" pt_pt = "Ajuda" ro = "Ajutor" ru = "Помощь" sr = "Pomoć" sv = "Hjälp" tk = "Kömek" tr = "Yardım" uk = "Допомога" vi = "Trợ giúp" zh_hans = "帮助" zh_hant = "幫助" [HelpAbout] en = "About" ar = "حول" bn = "সম্পর্কে" cs = "O programu" da = "Om" de = "Über" ee = "Teave" el = "Σχετικά με" es = "Acerca de" fa = "درباره" fi = "Tietoja" fr = "À propos" hu = "Névjegy" id = "Tentang" is = "Um" it = "Informazioni" ja = "情報" ko = "정보" nl = "Over" pl = "O programie" pt_br = "Sobre" pt_pt = "Sobre" ro = "Despre" ru = "О программе" sr = "O programu" sv = "Om" tk = "Programma barada" tr = "Hakkında" uk = "Про програму" vi = "Giới thiệu" zh_hans = "关于" zh_hant = "關於" [UnsavedChangesDialogTitle] en = "Unsaved Changes" ar = "تغييرات غير محفوظة" bn = "অসংরক্ষিত পরিবর্তন" cs = "Neuložené změny" da = "Ikke gemte ændringer" de = "Ungespeicherte Änderungen" ee = "Salvestamata muudatused" el = "Μη αποθηκευμένες αλλαγές" es = "Cambios sin guardar" fa = "تغییرات ذخیره‌نشده" fi = "Tallentamattomia muutoksia" fr = "Modifications non enregistrées" hu = "Mentetlen változtatások" id = "Perubahan yang Belum Disimpan" is = "Óvistaðar breytingar" it = "Modifiche non salvate" ja = "未保存の変更" ko = "저장되지 않은 변경 사항" nl = "Onopgeslagen Wijzigingen" pl = "Masz niezapisane zmiany" pt_br = "Alterações não Salvas" pt_pt = "Alterações não guardadas" ro = "Modificări nesalvate" ru = "Несохраненные изменения" sr = "Nesačuvane izmene" sv = "Osparade ändringar" tk = "Saklanmadyk üýtgeşmeler" tr = "Kaydedilmemiş Değişiklikler" uk = "Незбережені зміни" vi = "Các thay đổi chưa được lưu" zh_hans = "未保存的更改" zh_hant = "未儲存的變更" [UnsavedChangesDialogDescription] en = "Do you want to save the changes you made?" ar = "هل تريد حفظ التغييرات التي أجريتها؟" bn = "আপনি কি আপনার করা পরিবর্তনগুলি সংরক্ষণ করতে চান?" cs = "Chcete uložit provedené změny?" da = "Vil du gemme de ændringer, du har foretaget?" de = "Möchten Sie die vorgenommenen Änderungen speichern?" ee = "Kas sa soovid tehtud muudatused salvestada?" el = "Θέλετε να αποθηκεύσετε τις αλλαγές που κάνατε;" es = "¿Desea guardar los cambios realizados?" fa = "مایلید تغییرات ذخیره شود؟" fi = "Haluatko tallentaa tekemäsi muutokset?" fr = "Voulez-vous enregistrer les modifications apportées ?" hu = "Szeretnéd menteni a módosításokat?" id = "Apakah Anda ingin menyimpan perubahan yang Anda buat?" is = "Viltu vista breytingarnar sem þú gerðir?" it = "Vuoi salvare le modifiche apportate?" ja = "変更内容を保存しますか?" ko = "변경한 내용을 저장하시겠습니까?" nl = "Wilt u de gemaakte wijzigingen opslaan?" pl = "Czy chcesz zapisać wprowadzone modyfikacje?" pt_br = "Deseja salvar as alterações feitas?" pt_pt = "Quer guardar as alterações efetuadas?" ro = "Doriți să salvați modificările efectuate?" ru = "Вы хотите сохранить внесённые изменения?" sr = "Želite li da sačuvate izmene?" sv = "Vill du spara de ändringar du gjort?" tk = "Eden üýtgeşmeleriňizi saklamak isleýärsiňizmi?" tr = "Yaptığınız değişiklikleri kaydetmek istiyor musunuz?" uk = "Зберегти внесені зміни?" vi = "Bạn có muốn lưu các thay đổi bạn đã thực hiện không?" zh_hans = "您要保存所做的更改吗?" zh_hant = "您要保存所做的變更嗎?" [UnsavedChangesDialogYes] en = "Save" ar = "حفظ" bn = "সংরক্ষণ" cs = "Uložit" da = "Gem" de = "Speichern" ee = "Salvesta" el = "Αποθήκευση" es = "Guardar" fa = "ذخیره" fi = "Tallenna" fr = "Enregistrer" hu = "Mentés" id = "Simpan" is = "Vista" it = "Salva" ja = "保存する" ko = "저장" nl = "Opslaan" pl = "Zapisz" pt_br = "Salvar" pt_pt = "Guardar" ro = "Salvează" ru = "Сохранить" sr = "Sačuvaj" sv = "Spara" tk = "Sakla" tr = "Kaydet" uk = "Зберегти" vi = "Lưu" zh_hans = "保存" zh_hant = "儲存" [UnsavedChangesDialogNo] en = "Don't Save" ar = "لا تحفظ" bn = "সংরক্ষণ করবেন না" cs = "Neukládat" da = "Gem ikke" de = "Nicht speichern" ee = "Ära salvesta" el = "Να μην γίνει αποθήκευση" es = "No guardar" fi = "Älä tallenna" fr = "Ne pas enregistrer" hu = "Ne mentse" id = "Jangan Simpan" is = "Ekki vista" it = "Non salvare" ja = "保存しない" ko = "저장 안 함" nl = "Niet Opslaan" pl = "Nie zapisuj" pt_br = "Não Salvar" pt_pt = "Não guardar" ro = "Nu salva" ru = "Не сохранять" sr = "Nemoj sačuvati" sv = "Spara inte" tk = "Saklama" tr = "Kaydetme" uk = "Не зберігати" vi = "Không lưu" zh_hans = "不保存" zh_hant = "不儲存" [AboutDialogTitle] en = "About" ar = "حول" bn = "সম্পর্কে" cs = "O programu" da = "Om" de = "Über" ee = "Teave" el = "Σχετικά με" es = "Acerca de" fa = "درباره" fi = "Tietoja" fr = "À propos" hu = "Névjegy" id = "Tentang" is = "Um" it = "Informazioni" ja = "情報" ko = "정보" nl = "Over" pl = "O programie" pt_br = "Sobre" pt_pt = "Sobre" ro = "Despre" ru = "О программе" sr = "O programu" sv = "Om" tk = "Programma barada" tr = "Hakkında" uk = "Про програму" vi = "Giới thiệu" zh_hans = "关于" zh_hant = "關於" [AboutDialogVersion] en = "Version: " ar = "الإصدار: " bn = "সংস্করণ: " cs = "Verze: " da = "Version: " de = "Version: " ee = "Versioon: " el = "Έκδοση: " es = "Versión: " fa = "نسخه: " fi = "Versio: " fr = "Version : " hu = "Verzió: " id = "Versi: " is = "Útgáfa: " it = "Versione: " ja = "バージョン: " ko = "버전: " nl = "Versie: " pl = "Wersja: " pt_br = "Versão: " pt_pt = "Versão: " ro = "Versiune: " ru = "Версия: " sr = "Verzija: " sv = "Version: " tk = "Wersiýa: " tr = "Sürüm: " uk = "Версія: " vi = "Phiên bản: " 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." ar = "النص الذي تقوم بنسخه تتم مشاركته مع حافظة الطرفية." bn = "আপনার কপি করা টেক্সট টার্মিনাল ক্লিপবোর্ডের সাথে শেয়ার করা হয়।" cs = "Text, který zkopírujete, je sdílen s terminálovou schránkou." da = "Tekst, du kopierer, deles med terminalens udklipsholder." de = "Der kopierte Text wird mit der Terminal-Zwischenablage geteilt." ee = "Sinu kopeeritud tekst on jagatud terminali lõikelauaga." el = "Το κείμενο που αντιγράφετε κοινοποιείται με το πρόχειρο του τερματικού." es = "El texto que copies se comparte con el portapapeles del terminal." fa = "متن رونوشت شده با تخته یادداشت پایانه به اشتراک گذاشته شد." fi = "Kopioitu teksti jaetaan terminaalin leikepöydän kanssa." fr = "Le texte que vous copiez est partagé avec le presse-papiers du terminal." hu = "A kimásolt szöveg megosztásra kerül a terminál vágólapjával." id = "Teks yang Anda salin dibagikan dengan clipboard terminal." is = "Textanum sem þú afritar er deilt með skipanalínu-klippiborðinu." it = "Il testo copiato viene condiviso con gli appunti del terminale." ja = "コピーしたテキストはターミナルのクリップボードと共有されます。" ko = "복사한 텍스트는 터미널 클립보드와 공유됩니다." nl = "Tekst die u kopieert wordt gedeeld met de terminal." pl = "Skopiowany tekst jest przekazywany do schowka konsoli." pt_br = "O texto copiado é compartilhado com a área de transferência do terminal." pt_pt = "O texto que copia é partilhado com a área de transferência do terminal." ro = "Textul copiat este partajat cu clipboard-ul terminalului." ru = "Скопированный текст передаётся в буфер обмена терминала." sr = "Tekst koji kopirate se deli sa terminalom." sv = "Texten du kopierar delas med terminalens urklipp." tk = "Göçürilen ýazgy terminal paneline geçirilýär." tr = "Kopyaladığınız metin terminal panosuyla paylaşılır." uk = "Скопійований текст доступний в буфері обміну терміналу." vi = "Văn bản bạn sao chép sẽ được chia sẻ với bảng tạm của terminal." zh_hans = "你复制的文本将共享到终端剪贴板。" zh_hant = "您複製的文字將會與終端機剪貼簿分享。" # Shown when the clipboard size exceeds the limit for OSC 52 [LargeClipboardWarningLine2] en = "You copied {size} which may take a long time to share." ar = "لقد قمت بنسخ {size} مما قد يستغرق وقتًا طويلاً للمشاركة." bn = "আপনি {size} কপি করেছেন যা শেয়ার করতে দীর্ঘ সময় লাগতে পারে।" cs = "Zkopírovali jste {size}, což může trvat dlouho." da = "Du har kopieret {size}, som muligvis tager lang tid at dele." de = "Sie haben {size} kopiert. Das Weitergeben könnte länger dauern." ee = "Sa kopeerisid {size}, mis võib võtta kaua aega, et jagada." el = "Έχετε αντιγράψει {size}, η κοινοποίηση μπορεί να διαρκέσει πολύ." es = "Copiaste {size}, lo que puede tardar en compartirse." fa = "شما {size} رونوشت کرده‌اید که ممکن است برای اشتراک زمان گیرد." fi = "Kopioit {size} jonka jakamisessa voi kestää kauan." fr = "Vous avez copié {size}, ce qui peut être long à partager." hu = "{size} méretű adatot másoltál, amelynek megosztása hosszabb ideig tarthat." id = "Anda menyalin {size} yang mungkin butuh waktu lama untuk dibagikan." is = "Þú afritaðir {size}, sem gæti tekið langan tíma að deila." it = "Hai copiato {size}, potrebbe richiedere molto tempo per condividerlo." ja = "{size} をコピーしました。共有に時間がかかる可能性があります。" ko = "{size}를 복사했으며, 공유하는 데 시간이 오래 걸릴 수 있습니다." nl = "U heeft {size} gekopieerd, wat misschien lang kan duren om te delen." pl = "Skopiowano {size}, co może zająć dużo czasu." pt_br = "Você copiou {size}, o que pode demorar para compartilhar." pt_pt = "Copiou {size}, o que pode demorar a ser partilhado." ro = "Ai copiat {size}, ceea ce poate dura mult timp pentru a fi partajat." ru = "Вы скопировали {size}; передача может занять много времени." sr = "Kopirali ste {size}, deljenje može potrajati." sv = "Du kopierade {size}, vilket kan ta lång tid att dela." tk = "Paýlaşmak üçin köp wagt alyp bilýän {size} göçürdiňiz." tr = "{size} kopyaladınız, paylaşmak uzun sürebilir." uk = "Скопійовано {size}, передача може зайняти дещо часу." vi = "Bạn đã sao chép {size}, việc chia sẻ có thể mất nhiều thời gian." zh_hans = "你复制了 {size},共享可能需要较长时间。" zh_hant = "您已複製 {size},共享可能需要較長時間。" # Shown when the clipboard size exceeds the limit for OSC 52 [LargeClipboardWarningLine3] en = "Do you want to send it anyway?" ar = "هل تريد إرساله على أي حال؟" bn = "আপনি কি এটি যাইহোক পাঠাতে চান?" cs = "Chcete to přesto odeslat?" da = "Vil du sende det alligevel?" de = "Möchten Sie es trotzdem senden?" ee = "Kas soovid seda ikkagi saata?" el = "Θέλετε να το στείλετε ούτως ή άλλως;" es = "¿Desea enviarlo de todas formas?" fa = "مایلید آن را به هر روی ارسال کنید؟" fi = "Haluatko lähettää sen silti?" fr = "Voulez-vous quand même l’envoyer ?" hu = "El szeretnéd küldeni ennek ellenére?" id = "Apakah Anda tetap ingin mengirimkannya?" is = "Viltu samt senda það?" it = "Vuoi inviarlo comunque?" ja = "それでも送信しますか?" ko = "그래도 보내시겠습니까?" nl = "Wilt u het toch verzenden?" pl = "Czy nadal chcesz to przesłać?" pt_br = "Deseja enviar mesmo assim?" pt_pt = "Quer enviá-lo mesmo assim?" ro = "Vrei să-l trimiți oricum?" ru = "Отправить в любом случае?" sr = "Želite li da ga ipak pošaljete?" sv = "Vill du skicka det ändå?" tk = "Her niçigem bolsa ibermek isleýärsiňizmi?" tr = "Yine de göndermek istiyor musunuz?" uk = "Все одно надіслати?" vi = "Bạn vẫn muốn gửi chứ?" zh_hans = "仍要发送吗?" zh_hant = "仍要傳送嗎?" # As an alternative to LargeClipboardWarningLine2 and 3 [SuperLargeClipboardWarning] en = "The text you copied is too large to be shared." ar = "النص الذي قمت بنسخه كبير جدًا للمشاركة." bn = "আপনার কপি করা টেক্সট শেয়ার করার জন্য খুব বড়।" cs = "Text, který jste zkopírovali, je příliš velký na to, aby mohl být sdílen." da = "Den kopierede tekst er for stor til at blive delt." de = "Der kopierte Text ist zu groß, um geteilt zu werden." ee = "Sinu kopeeritud tekst on jagamiseks liiga suur." el = "Το κείμενο που αντιγράψατε είναι πολύ μεγάλο για να κοινοποιηθεί." es = "El texto que copiaste es demasiado grande para compartirse." fa = "متن رونوشت شده بززگتر از آن است که به اشتراک گذاشته شود." fi = "Kopioimasi on teksti on liian suuri jaettavaksi." fr = "Le texte que vous avez copié est trop volumineux pour être partagé." hu = "A kimásolt szöveg túl nagy a megosztáshoz." id = "Teks yang Anda salin terlalu besar untuk dibagikan." is = "Textinn sem þú afritaðir er of stór til að deila." it = "Il testo copiato è troppo grande per essere condiviso." ja = "コピーしたテキストは大きすぎて共有できません。" ko = "복사한 텍스트가 너무 커서 공유할 수 없습니다." nl = "De tekst die u kopieerde is te lang om te delen." pl = "Skopiowany tekst jest zbyt duży, żeby go przesłać." pt_br = "O texto copiado é grande demais para ser compartilhado." pt_pt = "O texto que copiou é demasiado grande para ser partilhado." ro = "Textul copiat este prea mare pentru a fi partajat." ru = "Скопированный текст слишком велик для передачи." sr = "Tekst koji ste kopirali je prevelik za deljenje." sv = "Texten du kopierade är för stor för att delas." tk = "Göçürilen ýazgy paýlaşmak üçin gaty uly." tr = "Kopyaladığınız metin paylaşmak için çok büyük." uk = "Скопійований текст завеликий для передачі." vi = "Văn bản bạn sao chép quá lớn, không thể chia sẻ." zh_hans = "你复制的文本过大,无法共享。" zh_hant = "您複製的文字過大,無法分享。" [WarningDialogTitle] en = "Warning" ar = "تحذير" bn = "সতর্কবার্তা" cs = "Upozornění" da = "Advarsel" de = "Warnung" ee = "Hoiatus" el = "Προειδοποίηση" es = "Advertencia" fa = "هشدار" fi = "Varoitus" fr = "Avertissement" hu = "Figyelmeztetés" id = "Peringatan" is = "Aðvörun" it = "Avviso" ja = "警告" ko = "경고" nl = "Waarschuwing" pl = "Ostrzeżenie" pt_br = "Aviso" pt_pt = "Aviso" ro = "Avertisment" ru = "Предупреждение" sr = "Upozorenje" sv = "Varning" tk = "Duýduryş" tr = "Uyarı" uk = "Попередження" vi = "Cảnh báo" zh_hans = "警告" zh_hant = "警告" [ErrorDialogTitle] en = "Error" ar = "خطأ" bn = "ত্রুটি" cs = "Chyba" da = "Fejl" de = "Fehler" ee = "Viga" el = "Σφάλμα" es = "Error" fa = "خطا" fi = "Virhe" fr = "Erreur" hu = "Hiba" id = "Galat" is = "Villa" it = "Errore" ja = "エラー" ko = "오류" nl = "Fout" pl = "Błąd" pt_br = "Erro" pt_pt = "Erro" ro = "Eroare" ru = "Ошибка" sr = "Greška" sv = "Fel" tk = "Ýalňyş" tr = "Hata" uk = "Помилка" vi = "Lỗi" zh_hans = "错误" zh_hant = "錯誤" [ErrorIcuMissing] en = "This operation requires the ICU library" ar = "تتطلب هذه العملية مكتبة ICU" bn = "এই অপারেশনের জন্য ICU লাইব্রেরি প্রয়োজন" cs = "Tato operace vyžaduje knihovnu ICU" da = "Denne operation kræver ICU-biblioteket" de = "Diese Operation erfordert die ICU-Bibliothek" ee = "See operatsioon nõuab ICU teeki" el = "Αυτή η λειτουργία απαιτεί τη βιβλιοθήκη ICU" es = "Esta operación requiere la biblioteca ICU" fa = "این عمل به کتابخانهٔ ICU نیاز دارد" fi = "Tämä toiminto vaatii ICU-kirjaston" fr = "Cette opération nécessite la bibliothèque ICU" hu = "Ezen művelethez szükséges az ICU könyvtár" id = "Operasi ini memerlukan pustaka ICU" is = "Þessi aðgerð krefst að ICU safnið sé til staðar" it = "Questa operazione richiede la libreria ICU" ja = "この操作にはICUライブラリが必要です" ko = "이 작업에는 ICU 라이브러리가 필요합니다" nl = "Deze bewerking vereist de ICU-bibliotheek" pl = "Ta operacja wymaga biblioteki ICU" pt_br = "Esta operação requer a biblioteca ICU" pt_pt = "Esta operação requer a biblioteca ICU" ro = "Această operațiune necesită biblioteca ICU" ru = "Эта операция требует наличия библиотеки ICU" sr = "Ova operacija zahteva ICU biblioteku" sv = "Denna åtgärd kräver ICU-biblioteket" tk = "Bu amal ICU kitaphanasyny talap edýär" tr = "Bu işlem ICU kütüphanesini gerektirir" uk = "Ця операція потребує наявності бібліотеки ICU" vi = "Thao tác này cần thư viện ICU" zh_hans = "此操作需要 ICU 库" zh_hant = "此操作需要 ICU 庫" # For input field [SearchNeedleLabel] en = "Find:" ar = "بحث:" bn = "অনুসন্ধান:" cs = "Najít:" da = "Find:" de = "Suchen:" ee = "Otsi:" el = "Εύρεση:" es = "Buscar:" fa = "جستجو:" fi = "Etsi:" fr = "Rechercher :" hu = "Keresés:" id = "Temukan:" is = "Finna:" it = "Trova:" ja = "検索:" ko = "찾기:" nl = "Zoeken:" pl = "Znajdź:" pt_br = "Localizar:" pt_pt = "Procurar:" ro = "Găsește:" ru = "Найти:" sr = "Pronađi:" sv = "Sök:" tk = "Gözle:" tr = "Bul:" uk = "Знайти:" vi = "Tìm:" zh_hans = "查找:" zh_hant = "尋找:" # For input field [SearchReplacementLabel] en = "Replace:" ar = "استبدال:" bn = "প্রতিস্থাপন:" cs = "Nahradit:" da = "Erstat:" de = "Ersetzen:" ee = "Asenda:" el = "Αντικατάσταση:" es = "Reemplazar:" fa = "جایگزین:" fi = "Korvaa:" fr = "Remplacer :" hu = "Csere:" id = "Ganti dengan:" is = "Skipta út með:" it = "Sostituire:" ja = "置換:" ko = "바꾸기:" nl = "Vervang:" pl = "Zamień:" pt_br = "Substituir:" pt_pt = "Substituir:" ro = "Înlocuiește:" ru = "Замена:" sr = "Zameni:" sv = "Ersätt:" tk = "Çalyş:" tr = "Değiştir:" uk = "Заміна:" vi = "Thay thế:" zh_hans = "替换:" zh_hant = "替換:" # Toggle [SearchMatchCase] en = "Match Case" ar = "مطابقة حالة الأحرف" bn = "কেস মিলান" cs = "Velká/malá písmena" da = "Store/små" de = "Groß/Klein" ee = "Jälgi suur- ja väiketähti" el = "Μεγάλα/μικρά" es = "May/Min" fa = "تطبیق حالت حروف" fi = "Ota huomioon kirjainkoko" fr = "Resp. la casse" hu = "Kisbetű/nagybetű" id = "Sesuaikan Jenis Huruf" is = "Greina há/lágstafi" it = "Maius/minus" ja = "大/小文字を区別" ko = "대소문자" nl = "Hoofdlettergevoelig" pl = "Wielkość liter" pt_br = "Maiús/Minús" pt_pt = "Diferenciar maiúsculas/minúsculas" ro = "Diferențiere Majuscule/minuscule" ru = "Регистр" sr = "Razlikuj mala/velika slova" sv = "Versaler" tk = "Gabat gelýän ýagdaýy" tr = "BÜYÜK/küçük harf" uk = "Регістр" vi = "Phân biệt chữ hoa/thường" zh_hans = "区分大小写" zh_hant = "區分大小寫" # Toggle [SearchWholeWord] en = "Whole Word" ar = "كلمة كاملة" bn = "সম্পূর্ণ শব্দ" cs = "Celé slovo" da = "Hele ordet" de = "Ganzes Wort" ee = "Kogu sõna" el = "Ολόκληρη λέξη" es = "Palabra" fa = "تمام کلمه" fi = "Koko sana" fr = "Mot entier" hu = "Teljes szó" id = "Seluruh Kata" is = "Heil orð" it = "Parola" ja = "単語全体" ko = "전체 단어" nl = "Volledig Woord" pl = "Całe słowa" pt_br = "Palavra" pt_pt = "Palavra inteira" ro = "Cuvânt întreg" ru = "Слово" sr = "Cela reč" sv = "Hela ord" tk = "Bütin söz" tr = "Tam sözcük" uk = "Ціле слово" vi = "Khớp toàn bộ từ" zh_hans = "全字匹配" zh_hant = "全字匹配" # Toggle [SearchUseRegex] en = "Use Regex" ar = "استخدام التعبيرات النمطية" bn = "Regex ব্যবহার করুন" cs = "Použít regulární výraz" da = "RegEx" de = "RegEx" ee = "RegEx" el = "RegEx" es = "RegEx" fa = "عبارت باقاعده" fi = "Regex" fr = "RegEx" hu = "RegEx" id = "Gunakan Regex" is = "Reglulegar segðir" it = "RegEx" ja = "正規表現" ko = "정규식" nl = "RegEx" pl = "RegEx" pt_br = "RegEx" pt_pt = "Expressão regular" ro = "RegEx" ru = "RegEx" sr = "Koristi regularni izraz" sv = "RegEx" tk = "Yzygiderli aňlatmalary ulan" tr = "RegEx kullan" uk = "RegEx" vi = "Sử dụng biểu thức chính quy" zh_hans = "正则" zh_hant = "正則" # Button [SearchReplaceAll] en = "Replace All" ar = "استبدال الكل" bn = "সব প্রতিস্থাপন করুন" cs = "Nahradit vše" da = "Erstat alle" de = "Alle ersetzen" ee = "Asenda kõik" el = "Αντικατάσταση όλων" es = "Reemplazar todo" fa = "جایگزینی همه" fi = "Korvaa kaikki" fr = "Remplacer tout" hu = "Az összes cseréje" id = "Ganti Semua" is = "Skipta öllu út" it = "Sostituisci tutto" ja = "すべて置換" ko = "모두 바꾸기" nl = "Vervang Alle" pl = "Zamień wszystko" pt_br = "Substituir Tudo" pt_pt = "Substituir tudo" ro = "Înlocuiește tot" ru = "Заменить все" sr = "Zameni sve" sv = "Ersätt alla" tk = "Hemmesini çalyş" tr = "Tümünü Değiştir" uk = "Замінити все" vi = "Thay thế tất cả" zh_hans = "全部替换" zh_hant = "全部取代" # Button [SearchClose] en = "Close" ar = "إغلاق" bn = "বন্ধ করুন" cs = "Zavřít" da = "Luk" de = "Schließen" ee = "Sulge" el = "Κλείσιμο" es = "Cerrar" fa = "بستن" fi = "Sulje" fr = "Fermer" hu = "Bezárás" id = "Tutup" is = "Loka" it = "Chiudi" ja = "閉じる" ko = "닫기" nl = "Sluiten" pl = "Zamknij" pt_br = "Fechar" pt_pt = "Fechar" ro = "Închide" ru = "Закрыть" sr = "Zatvori" sv = "Stäng" tk = "Ýap" tr = "Kapat" uk = "Закрити" vi = "Đóng" zh_hans = "关闭" zh_hant = "關閉" [EncodingReopen] en = "Reopen with encoding…" ar = "إعادة فتح مع الترميز…" bn = "এনকোডিং সহ পুনরায় খুলুন" cs = "Znovu otevřít s kódováním…" da = "Genåbn med enkodning" de = "Mit Kodierung erneut öffnen…" ee = "Ava uuesti kodeeringus…" el = "Επαναφορά με κωδικοποίηση…" es = "Reabrir con codificación…" fa = "با کدبندی مجدد باز کن" fi = "Avaa uudelleen koodauksella…" fr = "Rouvrir avec un encodage différent…" hu = "Újranyitás más kódolással…" id = "Buka kembali dengan pengkodean…" is = "Enduropna með stafakóðun" it = "Riapri con codifica…" ja = "指定エンコーディングで再度開く…" ko = "인코딩으로 다시 열기…" nl = "Heropen met codering…" pl = "Otwórz ponownie z kodowaniem…" pt_br = "Reabrir com a codificação…" pt_pt = "Reabrir com codificação…" ro = "Redeschide cu codificare…" ru = "Открыть снова с кодировкой…" sr = "Ponovo otvori sa kodiranjem" sv = "Öppna med enkodning" tk = "Kodlamak bilen gaýtadan aç" tr = "Kodlamayla Yeniden Aç…" uk = "Відкрити наново з кодуванням…" vi = "Mở lại với mã hóa…" zh_hans = "使用编码重新打开…" zh_hant = "使用編碼重新打開…" [EncodingConvert] en = "Convert to encoding…" ar = "تحويل إلى الترميز…" bn = "এনকোডিং-এ রূপান্তর করুন" cs = "Převést na kódování…" da = "Konverter til enkodning" de = "In Kodierung konvertieren…" ee = "Teisenda kodeeringusse…" el = "Μετατροπή σε κωδικοποίηση…" es = "Convertir a otra codificación…" fa = "تبدیل به کدبندی" fi = "Muunna koodaukselle…" fr = "Convertir vers l’encodage…" hu = "Átkonvertálás más karakterkódolásra…" id = "Konversi ke pengkodean…" is = "Umbreyta í stafakóðun" it = "Converti in codifica…" ja = "エンコーディングを変換…" ko = "인코딩으로 변환…" nl = "Zet om naar codering…" pl = "Konwertuj na kodowanie…" pt_br = "Converter para a codificação…" pt_pt = "Converter para codificação…" ro = "Convertire în codificare…" ru = "Преобразовать в кодировку…" sr = "Pretvori u kodiranje" sv = "Konvertera till enkodning" tk = "Koda öwür" tr = "Kodlamaya Dönüştür…" uk = "Перетворити в кодування…" vi = "Chuyển đổi sang mã hóa…" zh_hans = "转换为编码…" zh_hant = "轉換為編碼…" [IndentationTabs] en = "Tabs" ar = "علامات التبويب" bn = "ট্যাব" cs = "Tabulátory" da = "Tabs" de = "Tabs" ee = "Tab" el = "Tabs" es = "Tabulaciones" fa = "برگه‌ها" fi = "Sarkaimet" fr = "Tabulations" hu = "Tabulátorok" id = "Tab" is = "Bilstafir" it = "Tabulazioni" ja = "タブ" ko = "탭" nl = "Tabs" pl = "Tabulatory" pt_br = "Tabs" pt_pt = "Tabulações" ro = "Tabulări" ru = "Табы" sr = "Tabulatori" sv = "Flikar" tk = "Tabler" tr = "Sekme" uk = "Табуляція" vi = "Tab" zh_hans = "制表符" zh_hant = "製表符" [IndentationSpaces] en = "Spaces" ar = "المسافات" bn = "স্পেস" cs = "Mezery" da = "Mellemrum" de = "Leerzeichen" ee = "Tühikud" el = "Κενά" es = "Espacios" fa = "فاصله‌ها" fi = "Välilyönnit" fr = "Espaces" hu = "Szóközök" id = "Spasi" is = "Bil" it = "Spazi" ja = "スペース" ko = "공백" nl = "Spaties" pl = "Spacje" pt_br = "Espaços" pt_pt = "Espaços" ro = "Spații" ru = "Пробелы" sr = "Razmaci" sv = "Mellanslag" tk = "Boşluklar" tr = "Boşluk" uk = "Пробіли" vi = "Dấu cách" zh_hans = "空格" zh_hant = "空格" [SaveAsDialogPathLabel] en = "Folder:" ar = "المجلد:" bn = "ফোল্ডার:" cs = "Složka:" da = "Mappe:" de = "Ordner:" ee = "Kaust:" el = "Φάκελος:" es = "Carpeta:" fa = "پوشه:" fi = "Kansio:" fr = "Dossier :" hu = "Mappa:" id = "Folder:" is = "Mappa:" it = "Cartella:" ja = "フォルダ:" ko = "폴더:" nl = "Folder:" pl = "Lokalizacja:" pt_br = "Pasta:" pt_pt = "Pasta:" ro = "Dosar:" ru = "Папка:" sr = "Fascikla:" sv = "Mapp:" tk = "Bukja:" tr = "Klasör:" uk = "Папка:" vi = "Thư mục:" zh_hans = "文件夹:" zh_hant = "資料夾:" [SaveAsDialogNameLabel] en = "File name:" ar = "اسم الملف:" bn = "ফাইলের নাম:" cs = "Název souboru:" da = "Filnavn:" de = "Dateiname:" ee = "Faili nimi:" el = "Όνομα αρχείου:" es = "Nombre de archivo:" fa = "نام پرونده:" fi = "Tiedostonimi:" fr = "Nom du fichier :" hu = "Fájlnév:" id = "Nama file:" is = "Skráarnafn:" it = "Nome del file:" ja = "ファイル名:" ko = "파일 이름:" nl = "Bestandnaam:" pl = "Nazwa pliku:" pt_br = "Nome do Arquivo:" pt_pt = "Nome do ficheiro:" ro = "Nume fișier:" ru = "Имя файла:" sr = "Ime fajla:" sv = "Filnamn:" tk = "Faýl ady:" tr = "Dosya adı:" uk = "Ім'я файлу:" vi = "Tên tệp:" zh_hans = "文件名:" zh_hant = "檔案名稱:" [FileOverwriteWarning] en = "Confirm Save As" ar = "تأكيد الحفظ باسم…" bn = "অন্য নামে সংরক্ষণ নিশ্চিত করুন" cs = "Potvrdit Uložit jako" da = "Bekræft gem som" de = "Speichern unter bestätigen" ee = "Kinnita salvestamine kui" el = "Επιβεβαίωση αποθήκευσης ως…" es = "Confirmar Guardar como" fa = "پذیرفتن ذخیره به‌عنوان" fi = "Vahvista tallentaminen nimellä" fr = "Confirmer Enregistrer sous" hu = "Mentés másként megerősítése" id = "Konfirmasi Simpan Sebagai" is = "Staðfestu vista sem" it = "Conferma Salva con nome" ja = "名前を付けて保存の確認" ko = "다른 이름으로 저장 확인" nl = "Bevestig Opslaan Als" pl = "Potwierdź zapisanie jako" pt_br = "Confirmar Salvar Como" pt_pt = "Confirmar Guardar como" ro = "Confirmă Salvează ca" ru = "Подтвердите «Сохранить как…»" sr = "Potvrdi Sačuvaj kao" sv = "Bekräfta Spara som" tk = "Saklamagy tassykla…»" tr = "Farklı Kaydetmeyi Onayla" uk = "Підтвердьте «Зберегти як…»" vi = "Xác nhận Lưu thành" zh_hans = "确认另存为" zh_hant = "確認另存新檔" [FileOverwriteWarningDescription] en = "File already exists. Do you want to overwrite it?" ar = "الملف موجود بالفعل. هل تريد استبداله؟" bn = "ফাইল ইতিমধ্যে বিদ্যমান। আপনি কি এটি ওভাররাইট করতে চান?" cs = "Soubor již existuje. Chcete jej přepsat?" da = "Filen eksisterer allerede. Vil du overskrive den?" de = "Datei existiert bereits. Möchten Sie sie überschreiben?" ee = "Fail on juba olemas. Kas sa soovid selle üle kirjutada?" el = "Το αρχείο υπάρχει ήδη. Θέλετε να το αντικαταστήσετε;" es = "El archivo ya existe. ¿Desea sobrescribirlo?" fa = "پرونده در حال حاضر وجود دارد. مایلید آن را جایگزین کنید؟" fi = "Tiedosto on jo olemassa. Haluatko ylikirjoittaa sen?" fr = "Le fichier existe déjà. Voulez-vous l’écraser ?" hu = "A fájl már létezik. Felül szeretnéd írni?" id = "File sudah ada. Ingin menggantikannya?" is = "Skráin er þegar til. Viltu yfirskrifa hana?" it = "Il file esiste già. Vuoi sovrascriverlo?" ja = "ファイルは既に存在します。上書きしますか?" ko = "파일이 이미 존재합니다. 덮어쓰시겠습니까?" nl = "Bestand bestaat al. Wilt u het overschrijven?" pl = "Plik już istnieje. Czy chcesz go nadpisać?" pt_br = "O arquivo já existe. Deseja substituí-lo?" pt_pt = "O ficheiro já existe. Quer sobrescrevê-lo?" ro = "Fișierul există deja. Doriți să-l suprascrieți?" ru = "Файл уже существует. Перезаписать?" sr = "Fajl već postoji. Želite li da ga prepišete?" sv = "Filen finns redan. Vill du skriva över den?" tk = "Faýl eýýäm bar. Ony gaýtadan ýazmak isleýärsiňizmi?" tr = "Dosya zaten var. Üzerine yazmak istiyor musunuz?" uk = "Файл вже існує. Перезаписати?" vi = "Tệp đã tồn tại. Bạn có muốn ghi đè không?" zh_hans = "文件已存在。要覆盖它吗?" zh_hant = "檔案已存在。要覆蓋它嗎?" ================================================ FILE: rust-toolchain.toml ================================================ [toolchain] channel = "nightly" ================================================ FILE: rustfmt.toml ================================================ 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