diff --git a/pkgs/edit/.cargo/release-windows-ms.toml b/pkgs/edit/.cargo/release-windows-ms.toml
new file mode 100644
index 0000000..0e5f50a
--- /dev/null
+++ b/pkgs/edit/.cargo/release-windows-ms.toml
@@ -0,0 +1,32 @@
+# vvv The following parts are identical to release.toml vvv
+
+# Avoid linking with vcruntime140.dll by statically linking everything,
+# and then explicitly linking with ucrtbase.dll dynamically.
+# We do this, because vcruntime140.dll is an optional Windows component.
+[target.'cfg(target_os = "windows")']
+rustflags = [
+ "-Ctarget-feature=+crt-static",
+ "-Clink-args=/DEFAULTLIB:ucrt.lib",
+ "-Clink-args=/NODEFAULTLIB:vcruntime.lib",
+ "-Clink-args=/NODEFAULTLIB:msvcrt.lib",
+ "-Clink-args=/NODEFAULTLIB:libucrt.lib",
+]
+
+# The backtrace code for panics in Rust is almost as large as the entire editor.
+# = Huge reduction in binary size by removing all that.
+[unstable]
+build-std = ["std", "panic_abort"]
+build-std-features = ["panic_immediate_abort"]
+
+# vvv The following parts are specific to official Windows builds. vvv
+# (The use of internal registries, security features, etc., are mandatory.)
+
+# Enable shadow stacks: https://learn.microsoft.com/en-us/cpp/build/reference/cetcompat
+[target.'cfg(all(target_os = "windows", any(target_arch = "x86", target_arch = "x86_64")))']
+rustflags = ["-Clink-args=/DYNAMICBASE", "-Clink-args=/CETCOMPAT"]
+
+[registries.Edit_PublicPackages]
+index = "sparse+https://pkgs.dev.azure.com/microsoft/Dart/_packaging/Edit_PublicPackages/Cargo/index/"
+
+[source.crates-io]
+replace-with = "Edit_PublicPackages"
diff --git a/pkgs/edit/.cargo/release.toml b/pkgs/edit/.cargo/release.toml
new file mode 100644
index 0000000..790fde9
--- /dev/null
+++ b/pkgs/edit/.cargo/release.toml
@@ -0,0 +1,23 @@
+# The following is not used by default via .cargo/config.toml,
+# because `build-std-features` cannot be keyed by profile.
+# This breaks the bench profile which doesn't support panic=abort.
+# See: https://github.com/rust-lang/cargo/issues/11214
+# See: https://github.com/rust-lang/cargo/issues/13894
+
+# Avoid linking with vcruntime140.dll by statically linking everything,
+# and then explicitly linking with ucrtbase.dll dynamically.
+# We do this, because vcruntime140.dll is an optional Windows component.
+[target.'cfg(target_os = "windows")']
+rustflags = [
+ "-Ctarget-feature=+crt-static",
+ "-Clink-args=/DEFAULTLIB:ucrt.lib",
+ "-Clink-args=/NODEFAULTLIB:vcruntime.lib",
+ "-Clink-args=/NODEFAULTLIB:msvcrt.lib",
+ "-Clink-args=/NODEFAULTLIB:libucrt.lib",
+]
+
+# The backtrace code for panics in Rust is almost as large as the entire editor.
+# = Huge reduction in binary size by removing all that.
+[unstable]
+build-std = ["std", "panic_abort"]
+build-std-features = ["panic_immediate_abort"]
diff --git a/pkgs/edit/.gitignore b/pkgs/edit/.gitignore
new file mode 100644
index 0000000..c65c81a
--- /dev/null
+++ b/pkgs/edit/.gitignore
@@ -0,0 +1,5 @@
+.idea
+.vs
+*.profraw
+lcov.info
+target
diff --git a/pkgs/edit/.pipelines/release.yml b/pkgs/edit/.pipelines/release.yml
new file mode 100644
index 0000000..569ca77
--- /dev/null
+++ b/pkgs/edit/.pipelines/release.yml
@@ -0,0 +1,168 @@
+# Documentation: https://aka.ms/obpipelines
+
+trigger: none
+
+parameters:
+ - name: debug
+ displayName: Enable debug output
+ type: boolean
+ default: false
+ - name: official
+ displayName: Whether to build Official or NonOfficial
+ type: string
+ default: NonOfficial
+ values:
+ - NonOfficial
+ - Official
+ - name: createvpack
+ displayName: Enable vpack creation
+ type: boolean
+ default: false
+ - name: buildPlatforms
+ type: object
+ default:
+ - x86_64-pc-windows-msvc
+ - aarch64-pc-windows-msvc
+
+variables:
+ system.debug: ${{parameters.debug}}
+ WindowsContainerImage: onebranch.azurecr.io/windows/ltsc2022/vse2022:latest
+ # CDP_DEFINITION_BUILD_COUNT is needed for onebranch.pipeline.version task.
+ # See: https://aka.ms/obpipelines/versioning
+ CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)]
+ # LOAD BEARING - the vpack task fails without these
+ ROOT: $(Build.SourcesDirectory)
+ REPOROOT: $(Build.SourcesDirectory)
+ OUTPUTROOT: $(REPOROOT)\out
+ NUGET_XMLDOC_MODE: none
+
+resources:
+ repositories:
+ - repository: GovernedTemplates
+ type: git
+ name: OneBranch.Pipelines/GovernedTemplates
+ ref: refs/heads/main
+
+extends:
+ template: v2/Microsoft.${{parameters.official}}.yml@GovernedTemplates
+ parameters:
+ featureFlags:
+ WindowsHostVersion:
+ Version: 2022
+ Network: R1
+ platform:
+ name: windows_undocked
+ product: edit
+ # https://aka.ms/obpipelines/cloudvault
+ cloudvault:
+ enabled: false
+ # https://aka.ms/obpipelines/sdl
+ globalSdl:
+ binskim:
+ # > Due to some legacy reasons, 1ES PT is scanning full sources directory
+ # > for BinSkim tool instead of just scanning the output directory [...]
+ scanOutputDirectoryOnly: true
+ isNativeCode: true
+ tsa:
+ enabled: ${{eq(parameters.official, 'Official')}}
+ configFile: "$(Build.SourcesDirectory)/.pipelines/tsa.json"
+ stages:
+ # Our Build stage will build all three targets in one job, so we don't need
+ # to repeat most of the boilerplate work in three separate jobs.
+ - stage: Build
+ jobs:
+ - job: Windows
+ pool:
+ type: windows
+ variables:
+ # Binaries will go here.
+ # More settings at https://aka.ms/obpipelines/yaml/jobs
+ ob_outputDirectory: "$(Build.SourcesDirectory)/out"
+ # The vPack gets created from stuff in here.
+ # It will have a structure like:
+ # .../vpack/
+ # - amd64/
+ # - edit.exe
+ # - i386/
+ # - edit.exe
+ # - arm64/
+ # - edit.exe
+ ob_createvpack_enabled: ${{parameters.createvpack}}
+ ob_createvpack_vpackdirectory: "$(ob_outputDirectory)/vpack"
+ ob_createvpack_packagename: "windows_edit.$(Build.SourceBranchName)"
+ ob_createvpack_owneralias: lhecker@microsoft.com
+ ob_createvpack_description: Microsoft Edit
+ ob_createvpack_targetDestinationDirectory: "$(Destination)"
+ ob_createvpack_propsFile: false
+ ob_createvpack_provData: true
+ ob_createvpack_versionAs: string
+ ob_createvpack_version: "$(EditVersion)-$(CDP_DEFINITION_BUILD_COUNT)"
+ ob_createvpack_metadata: "$(Build.SourceVersion)"
+ ob_createvpack_topLevelRetries: 0
+ ob_createvpack_failOnStdErr: true
+ ob_createvpack_verbose: ${{ parameters.debug }}
+ # For details on this cargo_target_dir setting, see:
+ # https://eng.ms/docs/more/rust/topics/onebranch-workaround
+ CARGO_TARGET_DIR: C:\cargo_target_dir
+ # msrustup only supports stable toolchains, but this project requires nightly.
+ # We were told RUSTC_BOOTSTRAP=1 is a supported workaround.
+ RUSTC_BOOTSTRAP: 1
+ steps:
+ # NOTE: Step objects have ordered keys and you MUST have "task" as the first key.
+ # Objects with ordered keys... lol
+ - task: RustInstaller@1
+ displayName: Install Rust toolchain
+ inputs:
+ rustVersion: ms-stable
+ additionalTargets: x86_64-pc-windows-msvc aarch64-pc-windows-msvc
+ # URL of an Azure Artifacts feed configured with a crates.io upstream. Must be within the current ADO collection.
+ # NOTE: Azure Artifacts support for Rust is not yet public, but it is enabled for internal ADO organizations.
+ # https://learn.microsoft.com/en-us/azure/devops/artifacts/how-to/set-up-upstream-sources?view=azure-devops
+ cratesIoFeedOverride: sparse+https://pkgs.dev.azure.com/microsoft/Dart/_packaging/Edit_PublicPackages/Cargo/index/
+ # URL of an Azure Artifacts NuGet feed configured with the mscodehub Rust feed as an upstream.
+ # * The feed must be within the current ADO collection.
+ # * The CI account, usually "Project Collection Build Service (org-name)", must have at least "Collaborator" permission.
+ # When setting up the upstream NuGet feed, use following Azure Artifacts feed locator:
+ # azure-feed://mscodehub/Rust/Rust@Release
+ toolchainFeed: https://pkgs.dev.azure.com/microsoft/_packaging/RustTools/nuget/v3/index.json
+ - task: CargoAuthenticate@0
+ displayName: Authenticate with Azure Artifacts
+ inputs:
+ configFile: ".cargo/release-windows-ms.toml"
+ # We recommend making a separate `cargo fetch` step, as some build systems perform
+ # fetching entirely prior to the build, and perform the build with the network disabled.
+ - script: cargo fetch --config .cargo/release-windows-ms.toml
+ displayName: Fetch crates
+ - ${{ each platform in parameters.buildPlatforms }}:
+ - script: cargo build --config .cargo/release-windows-ms.toml --frozen --release --target ${{platform}}
+ displayName: Build ${{platform}} Release
+ - task: CopyFiles@2
+ displayName: Copy files to vpack (${{platform}})
+ inputs:
+ sourceFolder: "$(CARGO_TARGET_DIR)/${{platform}}/release"
+ ${{ if eq(platform, 'i686-pc-windows-msvc') }}:
+ targetFolder: "$(ob_createvpack_vpackdirectory)/i386"
+ ${{ elseif eq(platform, 'x86_64-pc-windows-msvc') }}:
+ targetFolder: "$(ob_createvpack_vpackdirectory)/amd64"
+ ${{ else }}: # aarch64-pc-windows-msvc
+ targetFolder: "$(ob_createvpack_vpackdirectory)/arm64"
+ contents: |
+ *.exe
+ *.pdb
+ # Extract the version for `ob_createvpack_version`.
+ - script: |-
+ @echo off
+ for /f "tokens=3 delims=- " %%x in ('findstr /c:"version = " Cargo.toml') do (
+ echo ##vso[task.setvariable variable=EditVersion]%%~x
+ goto :EOF
+ )
+ displayName: "Set EditVersion"
+ - task: onebranch.pipeline.signing@1
+ displayName: "Sign files"
+ inputs:
+ command: "sign"
+ signing_profile: "external_distribution"
+ files_to_sign: "**/edit.exe"
+ search_root: "$(ob_createvpack_vpackdirectory)"
+ use_testsign: false
+ in_container: true
diff --git a/pkgs/edit/.pipelines/tsa.json b/pkgs/edit/.pipelines/tsa.json
new file mode 100644
index 0000000..0e1f9b0
--- /dev/null
+++ b/pkgs/edit/.pipelines/tsa.json
@@ -0,0 +1,7 @@
+{
+ "instanceUrl": "https://microsoft.visualstudio.com",
+ "projectName": "OS",
+ "areaPath": "OS\\Windows Client and Services\\WinPD\\DFX-Developer Fundamentals and Experiences\\DEFT\\SHINE\\Commandline Tooling",
+ "notificationAliases": ["condev@microsoft.com", "duhowett@microsoft.com"],
+ "template": "VSTS_Microsoft_OSGS"
+}
diff --git a/pkgs/edit/.vscode/launch.json b/pkgs/edit/.vscode/launch.json
new file mode 100644
index 0000000..b57b8b7
--- /dev/null
+++ b/pkgs/edit/.vscode/launch.json
@@ -0,0 +1,28 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Launch Debug (Windows)",
+ "preLaunchTask": "rust: cargo build",
+ "type": "cppvsdbg",
+ "request": "launch",
+ "console": "externalTerminal",
+ "program": "${workspaceFolder}/target/debug/edit",
+ "cwd": "${workspaceFolder}",
+ "args": [
+ "${workspaceFolder}/src/bin/edit/main.rs"
+ ],
+ },
+ {
+ "name": "Launch Debug (GDB/LLDB)",
+ "preLaunchTask": "rust: cargo build",
+ "type": "cppdbg",
+ "request": "launch",
+ "program": "${workspaceFolder}/target/debug/edit",
+ "cwd": "${workspaceFolder}",
+ "args": [
+ "${workspaceFolder}/src/bin/edit/main.rs"
+ ],
+ }
+ ]
+}
diff --git a/pkgs/edit/.vscode/tasks.json b/pkgs/edit/.vscode/tasks.json
new file mode 100644
index 0000000..daeaf8c
--- /dev/null
+++ b/pkgs/edit/.vscode/tasks.json
@@ -0,0 +1,24 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "rust: cargo build",
+ "type": "process",
+ "command": "cargo",
+ "args": [
+ "build",
+ "--package",
+ "edit",
+ "--features",
+ "debug-latency"
+ ],
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ },
+ "problemMatcher": [
+ "$rustc"
+ ]
+ }
+ ]
+}
diff --git a/pkgs/edit/CODE_OF_CONDUCT.md b/pkgs/edit/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..686e5e7
--- /dev/null
+++ b/pkgs/edit/CODE_OF_CONDUCT.md
@@ -0,0 +1,10 @@
+# Microsoft Open Source Code of Conduct
+
+This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
+
+Resources:
+
+- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
+- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
+- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
+- Employees can reach out at [aka.ms/opensource/moderation-support](https://aka.ms/opensource/moderation-support)
diff --git a/pkgs/edit/CONTRIBUTING.md b/pkgs/edit/CONTRIBUTING.md
new file mode 100644
index 0000000..1398694
--- /dev/null
+++ b/pkgs/edit/CONTRIBUTING.md
@@ -0,0 +1,49 @@
+# Contributing
+
+## Translation improvements
+
+You can find our translations in [`src/bin/edit/localization.rs`](./src/bin/edit/localization.rs).
+Please feel free to open a pull request with your changes at any time.
+If you'd like to discuss your changes first, please feel free to open an issue.
+
+## Bug reports
+
+If you find any bugs, we gladly accept pull requests without prior discussion.
+Otherwise, you can of course always open an issue for us to look into.
+
+## Feature requests
+
+Please open a new issue for any feature requests you have in mind.
+Keeping the binary size of the editor small is a priority for us and so we may need to discuss any new features first until we have support for plugins.
+
+## Code changes
+
+The project has a focus on a small binary size and sufficient (good) performance.
+As such, we generally do not accept pull requests that introduce dependencies (there are always exceptions of course).
+Otherwise, you can consider this project a playground for trying out any cool ideas you have.
+
+The overall architecture of the project can be summarized as follows:
+* The underlying text buffer in `src/buffer` doesn't keep track of line breaks in the document.
+ This is a crucial design aspect that permeates throughout the entire codebase.
+
+ To oversimplify, the *only* state that is kept is the current cursor position.
+ When the user asks to move to another line, the editor will `O(n)` seek through the underlying document until it found the corresponding number of line breaks.
+ * As a result, `src/simd` contains crucial `memchr2` functions to quickly find the next or previous line break (runs at up to >100GB/s).
+ * Furthermore, `src/unicode` implements an `Utf8Chars` iterator which transparently inserts U+FFFD replacements during iteration (runs at up to 4GB/s).
+ * Furthermore, `src/unicode` also implements grapheme cluster segmentation and cluster width measurement via its `MeasurementConfig` (runs at up to 600MB/s).
+ * If word wrap is disabled, `memchr2` is used for all navigation across lines, allowing us to breeze through 1GB large files as if they were 1MB.
+ * Even if word-wrap is enabled, it's still sufficiently smooth thanks to `MeasurementConfig`. This is only possible because these base functions are heavily optimized.
+* `src/framebuffer.rs` implements a "framebuffer" like in video games.
+ It allows us to draw the UI output into an intermediate buffer first, accumulating all changes and handling things like color blending.
+ Then, it can compare the accumulated output with the previous frame and only send the necessary changes to the terminal.
+* `src/tui.rs` implements an immediate mode UI. Its module implementation gives an overview how it works and I recommend reading it.
+* `src/vt.rs` implements our VT parser.
+* `src/sys` contains our platform abstractions.
+* Finally, `src/bin/edit` ties everything together.
+ It's roughly 90% UI code and business logic.
+ It contains a little bit of VT logic in `setup_terminal`.
+
+If you have an issue with your terminal, the places of interest are the aforementioned:
+* VT parser in `src/vt.rs`
+* Platform specific code in `src/sys`
+* And the `setup_terminal` function in `src/bin/edit/main.rs`
diff --git a/pkgs/edit/Cargo.lock b/pkgs/edit/Cargo.lock
new file mode 100644
index 0000000..87664f5
--- /dev/null
+++ b/pkgs/edit/Cargo.lock
@@ -0,0 +1,627 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anes"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
+
+[[package]]
+name = "anstyle"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
+
+[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
+[[package]]
+name = "bumpalo"
+version = "3.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
+
+[[package]]
+name = "cast"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "ciborium"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
+dependencies = [
+ "ciborium-io",
+ "ciborium-ll",
+ "serde",
+]
+
+[[package]]
+name = "ciborium-io"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
+
+[[package]]
+name = "ciborium-ll"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
+dependencies = [
+ "ciborium-io",
+ "half",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944"
+dependencies = [
+ "clap_builder",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9"
+dependencies = [
+ "anstyle",
+ "clap_lex",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
+
+[[package]]
+name = "criterion"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
+dependencies = [
+ "anes",
+ "cast",
+ "ciborium",
+ "clap",
+ "criterion-plot",
+ "is-terminal",
+ "itertools",
+ "num-traits",
+ "once_cell",
+ "oorandom",
+ "plotters",
+ "rayon",
+ "regex",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "tinytemplate",
+ "walkdir",
+]
+
+[[package]]
+name = "criterion-plot"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
+dependencies = [
+ "cast",
+ "itertools",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crunchy"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
+
+[[package]]
+name = "edit"
+version = "1.0.0"
+dependencies = [
+ "criterion",
+ "libc",
+ "windows-sys",
+ "winres",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "half"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e"
+
+[[package]]
+name = "is-terminal"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.171"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
+
+[[package]]
+name = "log"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "oorandom"
+version = "11.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
+
+[[package]]
+name = "plotters"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
+dependencies = [
+ "num-traits",
+ "plotters-backend",
+ "plotters-svg",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "plotters-backend"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
+
+[[package]]
+name = "plotters-svg"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
+dependencies = [
+ "plotters-backend",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.94"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rayon"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
+[[package]]
+name = "rustversion"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.140"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tinytemplate"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "toml"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "winapi-util"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "winres"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c"
+dependencies = [
+ "toml",
+]
diff --git a/pkgs/edit/Cargo.toml b/pkgs/edit/Cargo.toml
new file mode 100644
index 0000000..05872fc
--- /dev/null
+++ b/pkgs/edit/Cargo.toml
@@ -0,0 +1,53 @@
+[package]
+name = "edit"
+version = "1.0.0"
+edition = "2024"
+
+[[bench]]
+name = "lib"
+harness = false
+
+[features]
+debug-layout = []
+debug-latency = []
+
+# `opt-level = "s"` may be useful in the future as it significantly reduces binary size.
+# We could then use the `#[optimize(speed)]` attribute for spot optimizations.
+# Unfortunately, that attribute currently doesn't work on intrinsics such as memset.
+[profile.release]
+codegen-units = 1 # reduces binary size by ~2%
+debug = "full" # No one needs an undebuggable release binary
+lto = true # reduces binary size by ~14%
+opt-level = "s" # reduces binary size by ~25%
+panic = "abort" # reduces binary size by ~50% in combination with -Zbuild-std-features=panic_immediate_abort
+split-debuginfo = "packed" # generates a seperate *.dwp/*.dSYM so the binary can get stripped
+strip = "symbols" # See split-debuginfo - allows us to drop the size by ~65%
+
+[profile.bench]
+codegen-units = 16 # Make compiling criterion faster (16 is the default, but profile.release sets it to 1)
+lto = "thin" # Similarly, speed up linking by a ton
+
+[dependencies]
+
+[target.'cfg(unix)'.dependencies]
+libc = "0.2"
+
+[target.'cfg(windows)'.build-dependencies]
+winres = "0.1"
+
+[target.'cfg(windows)'.dependencies.windows-sys]
+version = "0.59"
+features = [
+ "Win32_Globalization",
+ "Win32_Security",
+ "Win32_Storage_FileSystem",
+ "Win32_System_Console",
+ "Win32_System_Diagnostics_Debug",
+ "Win32_System_IO",
+ "Win32_System_LibraryLoader",
+ "Win32_System_Memory",
+ "Win32_System_Threading",
+]
+
+[dev-dependencies]
+criterion = { version = "0.5", features = ["html_reports"] }
diff --git a/pkgs/edit/LICENSE b/pkgs/edit/LICENSE
new file mode 100644
index 0000000..88040d8
--- /dev/null
+++ b/pkgs/edit/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) Microsoft Corporation. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/pkgs/edit/README.md b/pkgs/edit/README.md
new file mode 100644
index 0000000..acd0947
--- /dev/null
+++ b/pkgs/edit/README.md
@@ -0,0 +1,22 @@
+#  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.
+
+
+
+## Installation
+
+* Download the latest release from our [releases page](https://github.com/microsoft/edit/releases/latest)
+* Extract the archive
+* Copy the `edit` binary to a directory in your `PATH`
+* You may delete any other files in the archive if you don't need them
+
+## Build Instructions
+
+* [Install Rust](https://www.rust-lang.org/tools/install)
+* Install the nightly toolchain: `rustup install nightly`
+ * Alternatively, set the environment variable `RUSTC_BOOTSTRAP=1`
+* Clone the repository
+* For a release build, run: `cargo build --config .cargo/release.toml --release`
diff --git a/pkgs/edit/SECURITY.md b/pkgs/edit/SECURITY.md
new file mode 100644
index 0000000..82db58a
--- /dev/null
+++ b/pkgs/edit/SECURITY.md
@@ -0,0 +1,41 @@
+
+
+## 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).
+
+
diff --git a/pkgs/edit/assets/Microsoft_logo_(1980).svg b/pkgs/edit/assets/Microsoft_logo_(1980).svg
new file mode 100644
index 0000000..473c8d5
--- /dev/null
+++ b/pkgs/edit/assets/Microsoft_logo_(1980).svg
@@ -0,0 +1,26 @@
+
+
+
+
diff --git a/pkgs/edit/assets/edit.svg b/pkgs/edit/assets/edit.svg
new file mode 100644
index 0000000..74a6242
--- /dev/null
+++ b/pkgs/edit/assets/edit.svg
@@ -0,0 +1,75 @@
+
diff --git a/pkgs/edit/assets/edit_hero_image.png b/pkgs/edit/assets/edit_hero_image.png
new file mode 100644
index 0000000..87bed35
Binary files /dev/null and b/pkgs/edit/assets/edit_hero_image.png differ
diff --git a/pkgs/edit/assets/microsoft.png b/pkgs/edit/assets/microsoft.png
new file mode 100644
index 0000000..1092d4c
Binary files /dev/null and b/pkgs/edit/assets/microsoft.png differ
diff --git a/pkgs/edit/assets/microsoft.sixel b/pkgs/edit/assets/microsoft.sixel
new file mode 100644
index 0000000..bf53919
--- /dev/null
+++ b/pkgs/edit/assets/microsoft.sixel
@@ -0,0 +1 @@
+P;1q"1;1;300;60#0;2;100;100;100#0!42?_ow{}!12?_ow{}!6?_ow{}}!5?_ow{{}}}!17~^NFbpw{}!8~!4}{wwo_!12?_oow{{{!4}!6~!4}{{wwo__!4?_ow{{}}}!23~^Nfrxw{{}}}!9~!4}{{woo_!12?_ow{}!15~^NFbpw{}!17~^NFB@-!36?_ow{}!6~!6?_ow{}!6~??w{}!7~?o{}!10~^^!10NFBpw{}!6~!8N^!9~{_!4?_o{}!8~^^!9N^^!9~{w}!8~^!18NFbx{}!9~^^!8N^^!9~}{o???ow{}!6~!11NFB@GKM!5N!10~!4NFB@-!30?_ow{}!12~_ow{}!12~??!20~FB@!15?!10~!10?r!9~???{!8~NB@!15?@FN!16~!4{!4wooo__!5?_}!8~^FB!16?@F^!8~{o!10~!9o!13?!10~-!24?_ow{}!35~??!19~x!18?!10~?CK[!4{}!9~^B??N!8~x!21?!10~N^^!18~}{o!10~!22?!29~!13?!10~-!18?_ow{}!8~^NFB@?!11~^NFB@?!10~??!10~F!9~}{wo__!12?!10~!5?@BFN^!9~}{wof^!7~}wo__!11?__o{!9~N@!7?!6@Bb!10~N!9~{o__!12?__o{}!8~F@!10~!9B!13?!10~-!12?_ow{}!8~^NFB@!7?!5~^NFB@!7?!10~??!10~??@FN^!20~??!10~!11?@BFN^!23~!7}!10~^NFB~!12}!12~^NB??BFN^!9~!10}!9~^NF@???!10~!22?!5~^NFB@-!6?_ow{}!8~^NFB@!13?FFB@!13?!10F??!10F!7?@@BB!15F??!10F!17?@BFN^!10~|zrfFF!10NFFFBB@@!5?!21FBB@!11?@BBFFNNN!10^NNNFFBB@!8?!10~!22?NFB@-_ow{}!8~^NFB@!119?@BFN^!9~}{wo!88?!10~-!7~^NFB@!131?@BFN^!7~!88?!7~^NF-~^NFB@!143?@BFN^~!88?~^NFB@\
diff --git a/pkgs/edit/benches/lib.rs b/pkgs/edit/benches/lib.rs
new file mode 100644
index 0000000..1131f34
--- /dev/null
+++ b/pkgs/edit/benches/lib.rs
@@ -0,0 +1,116 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use std::hint::black_box;
+use std::mem;
+
+use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
+use edit::helpers::*;
+use edit::simd::MemsetSafe;
+use edit::{hash, oklab, simd, unicode};
+
+fn bench_hash(c: &mut Criterion) {
+ c.benchmark_group("hash")
+ .throughput(Throughput::Bytes(8))
+ .bench_function(BenchmarkId::new("hash", 8), |b| {
+ let data = [0u8; 8];
+ b.iter(|| hash::hash(0, black_box(&data)))
+ })
+ .throughput(Throughput::Bytes(16))
+ .bench_function(BenchmarkId::new("hash", 16), |b| {
+ let data = [0u8; 16];
+ b.iter(|| hash::hash(0, black_box(&data)))
+ })
+ .throughput(Throughput::Bytes(1024))
+ .bench_function(BenchmarkId::new("hash", 1024), |b| {
+ let data = [0u8; 1024];
+ b.iter(|| hash::hash(0, black_box(&data)))
+ });
+}
+
+fn bench_oklab(c: &mut Criterion) {
+ c.benchmark_group("oklab")
+ .bench_function("srgb_to_oklab", |b| b.iter(|| oklab::srgb_to_oklab(black_box(0xff212cbe))))
+ .bench_function("oklab_blend", |b| {
+ b.iter(|| oklab::oklab_blend(black_box(0x7f212cbe), black_box(0x7f3aae3f)))
+ });
+}
+
+fn bench_simd_memchr2(c: &mut Criterion) {
+ let mut group = c.benchmark_group("simd");
+ let mut buffer_u8 = [0u8; 2048];
+
+ for &bytes in &[8usize, 32 + 8, 64 + 8, KIBI + 8] {
+ group.throughput(Throughput::Bytes(bytes as u64 + 1)).bench_with_input(
+ BenchmarkId::new("memchr2", bytes),
+ &bytes,
+ |b, &size| {
+ buffer_u8.fill(b'a');
+ buffer_u8[size] = b'\n';
+ b.iter(|| simd::memchr2(b'\n', b'\r', black_box(&buffer_u8), 0));
+ },
+ );
+ }
+}
+
+fn bench_simd_memset(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(); 2048 / size];
+
+ for &bytes in &[8usize, 32 + 8, 64 + 8, KIBI + 8] {
+ group.throughput(Throughput::Bytes(bytes as u64)).bench_with_input(
+ BenchmarkId::new(&name, bytes),
+ &bytes,
+ |b, &bytes| {
+ let slice = unsafe { buf.get_unchecked_mut(..bytes / size) };
+ b.iter(|| simd::memset(black_box(slice), Default::default()));
+ },
+ );
+ }
+}
+
+fn bench_unicode(c: &mut Criterion) {
+ let reference = concat!(
+ "In the quiet twilight, dreams unfold, soft whispers of a story untold.\n",
+ "月明かりが静かに照らし出し、夢を見る心の奥で詩が静かに囁かれる\n",
+ "Stars collide in the early light of hope, echoing the silent call of the night.\n",
+ "夜の静寂、希望と孤独が混ざり合うその中で詩が永遠に続く\n",
+ );
+ let buffer = reference.repeat(10);
+ let bytes = buffer.as_bytes();
+
+ c.benchmark_group("unicode::MeasurementConfig::goto_logical")
+ .throughput(Throughput::Bytes(bytes.len() as u64))
+ .bench_function("basic", |b| {
+ b.iter(|| unicode::MeasurementConfig::new(&bytes).goto_logical(Point::MAX))
+ })
+ .bench_function("word_wrap", |b| {
+ b.iter(|| {
+ unicode::MeasurementConfig::new(black_box(&bytes))
+ .with_word_wrap_column(50)
+ .goto_logical(Point::MAX)
+ })
+ });
+
+ c.benchmark_group("unicode::Utf8Chars")
+ .throughput(Throughput::Bytes(bytes.len() as u64))
+ .bench_function("next", |b| {
+ b.iter(|| {
+ unicode::Utf8Chars::new(bytes, 0).fold(0u32, |acc, ch| acc.wrapping_add(ch as u32))
+ })
+ });
+}
+
+fn bench(c: &mut Criterion) {
+ bench_hash(c);
+ bench_oklab(c);
+ bench_simd_memchr2(c);
+ bench_simd_memset::(c);
+ bench_simd_memset::(c);
+ bench_unicode(c);
+}
+
+criterion_group!(benches, bench);
+criterion_main!(benches);
diff --git a/pkgs/edit/build.rs b/pkgs/edit/build.rs
new file mode 100644
index 0000000..2e52f02
--- /dev/null
+++ b/pkgs/edit/build.rs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+fn main() {
+ #[cfg(windows)]
+ if std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default() == "windows" {
+ winres::WindowsResource::new()
+ .set_manifest_file("src/bin/edit/edit.exe.manifest")
+ .set("FileDescription", "Microsoft Edit")
+ .set("LegalCopyright", "© Microsoft Corporation. All rights reserved.")
+ .compile()
+ .unwrap();
+ }
+}
diff --git a/pkgs/edit/edit.nix b/pkgs/edit/edit.nix
new file mode 100644
index 0000000..4e6684b
--- /dev/null
+++ b/pkgs/edit/edit.nix
@@ -0,0 +1,27 @@
+{
+ lib,
+ pkgs
+}:
+
+let
+ rustPlatform = pkgs.makeRustPlatform {
+ cargo = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default);
+ rustc = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default);
+ };
+in
+
+rustPlatform.buildRustPackage (finalAttrs: {
+ pname = "edit";
+ version = "1.0.0";
+
+ src = ./.;
+
+ cargoHash = "sha256-DEzjfrXSmum/GJdYanaRDKxG4+eNPWf5echLhStxcIg=";
+
+ meta = {
+ description = "We all edit.";
+ homepage = "https://github.com/microsoft/edit";
+ license = lib.licenses.mit;
+ maintainers = [ ];
+ };
+})
diff --git a/pkgs/edit/flake.lock b/pkgs/edit/flake.lock
new file mode 100644
index 0000000..c001d69
--- /dev/null
+++ b/pkgs/edit/flake.lock
@@ -0,0 +1,99 @@
+{
+ "nodes": {
+ "fenix": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ],
+ "rust-analyzer-src": "rust-analyzer-src"
+ },
+ "locked": {
+ "lastModified": 1747392669,
+ "narHash": "sha256-zky3+lndxKRu98PAwVK8kXPdg+Q1NVAhaI7YGrboKYA=",
+ "owner": "nix-community",
+ "repo": "fenix",
+ "rev": "c3c27e603b0d9b5aac8a16236586696338856fbb",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-community",
+ "repo": "fenix",
+ "type": "github"
+ }
+ },
+ "flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1747744144,
+ "narHash": "sha256-W7lqHp0qZiENCDwUZ5EX/lNhxjMdNapFnbErcbnP11Q=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "2795c506fe8fb7b03c36ccb51f75b6df0ab2553f",
+ "type": "github"
+ },
+ "original": {
+ "id": "nixpkgs",
+ "ref": "nixos-unstable",
+ "type": "indirect"
+ }
+ },
+ "root": {
+ "inputs": {
+ "fenix": "fenix",
+ "flake-utils": "flake-utils",
+ "nixpkgs": "nixpkgs"
+ }
+ },
+ "rust-analyzer-src": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1747323949,
+ "narHash": "sha256-G4NwzhODScKnXqt2mEQtDFOnI0wU3L1WxsiHX3cID/0=",
+ "owner": "rust-lang",
+ "repo": "rust-analyzer",
+ "rev": "f8e784353bde7cbf9a9046285c1caf41ac484ebe",
+ "type": "github"
+ },
+ "original": {
+ "owner": "rust-lang",
+ "ref": "nightly",
+ "repo": "rust-analyzer",
+ "type": "github"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/pkgs/edit/flake.nix b/pkgs/edit/flake.nix
new file mode 100644
index 0000000..a6fe65a
--- /dev/null
+++ b/pkgs/edit/flake.nix
@@ -0,0 +1,39 @@
+{
+ description = "Flake containing Edit";
+
+ inputs = {
+ fenix = {
+ url = "github:nix-community/fenix";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+ flake-utils.url = "github:numtide/flake-utils";
+ nixpkgs.url = "nixpkgs/nixos-unstable";
+ };
+
+ outputs = { self, fenix, flake-utils, nixpkgs }:
+ flake-utils.lib.eachDefaultSystem (system:
+ let pkgs = nixpkgs.legacyPackages.${system}; in
+ {
+ defaultPackage =
+ let rustPlatform = (pkgs.makeRustPlatform {
+ inherit (fenix.packages.${system}.minimal) cargo rustc;
+ });
+ in rustPlatform.buildRustPackage {
+ pname = "edit";
+ version = "1.0.0";
+
+ src = ./.;
+
+ cargoHash = "sha256-DEzjfrXSmum/GJdYanaRDKxG4+eNPWf5echLhStxcIg=";
+
+ meta = {
+ description = "We all edit.";
+ homepage = "https://github.com/microsoft/edit";
+ license = pkgs.lib.licenses.mit;
+ maintainers = [ ];
+ };
+
+ };
+ }
+ );
+}
diff --git a/pkgs/edit/hi.nix b/pkgs/edit/hi.nix
new file mode 100644
index 0000000..8ea921d
--- /dev/null
+++ b/pkgs/edit/hi.nix
@@ -0,0 +1,62 @@
+{
+ description = "core-lending";
+
+ inputs = {
+ nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
+ flake-parts.url = "github:hercules-ci/flake-parts";
+ rust-overlay = {
+ url = "github:oxalica/rust-overlay";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+ };
+
+ outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } {
+ systems = [
+ "x86_64-linux"
+ "aarch64-darwin"
+ ];
+
+ perSystem = { self', pkgs, system, ... }:
+ let
+ rustVersion = "1.86.0";
+ rust = pkgs.rust-bin.stable.${rustVersion};
+ rustPlatform = pkgs.makeRustPlatform {
+ rustc = rust.minimal;
+ cargo = rust.minimal;
+ };
+ cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
+ in
+ {
+ _module.args.pkgs = import inputs.nixpkgs {
+ inherit system;
+ overlays = [ inputs.rust-overlay.overlays.default ];
+ };
+
+ packages.default = rustPlatform.buildRustPackage {
+ pname = cargoToml.package.name;
+ version = cargoToml.package.version;
+
+ src = ./.;
+ cargoLock = {
+ lockFile = ./Cargo.lock;
+ };
+ };
+
+ devShells.default = pkgs.mkShell {
+ buildInputs = [
+ pkgs.diesel-cli
+ pkgs.elmPackages.elm
+ pkgs.elmPackages.elm-language-server
+ pkgs.postgresql_17
+ (rust.default.override {
+ extensions = [ "rust-analyzer" "rust-src" ];
+ })
+ ];
+
+ shellHook = ''
+ export LANG="en_US.UTF-8"
+ '';
+ };
+ };
+ };
+}
diff --git a/pkgs/edit/result b/pkgs/edit/result
new file mode 120000
index 0000000..7cba96b
--- /dev/null
+++ b/pkgs/edit/result
@@ -0,0 +1 @@
+/nix/store/inr2b18j80dc17bkaz5xm7f20g0i3s0r-edit-1.0.0
\ No newline at end of file
diff --git a/pkgs/edit/rustfmt.toml b/pkgs/edit/rustfmt.toml
new file mode 100644
index 0000000..90f2491
--- /dev/null
+++ b/pkgs/edit/rustfmt.toml
@@ -0,0 +1,7 @@
+style_edition = "2024"
+use_small_heuristics = "Max"
+group_imports = "StdExternalCrate"
+imports_granularity = "Module"
+format_code_in_doc_comments = true
+newline_style = "Unix"
+use_field_init_shorthand = true
diff --git a/pkgs/edit/src/apperr.rs b/pkgs/edit/src/apperr.rs
new file mode 100644
index 0000000..b1dd6d2
--- /dev/null
+++ b/pkgs/edit/src/apperr.rs
@@ -0,0 +1,42 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! Provides a transparent error type for edit.
+
+use std::{io, result};
+
+use crate::sys;
+
+pub const APP_ICU_MISSING: Error = Error::new_app(0);
+
+/// Edit's transparent `Result` type.
+pub type Result = result::Result;
+
+/// Edit's transparent `Error` type.
+/// Abstracts over system and application errors.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Error {
+ App(u32),
+ Icu(u32),
+ Sys(u32),
+}
+
+impl Error {
+ pub const fn new_app(code: u32) -> Self {
+ Error::App(code)
+ }
+
+ pub const fn new_icu(code: u32) -> Self {
+ Error::Icu(code)
+ }
+
+ pub const fn new_sys(code: u32) -> Self {
+ Error::Sys(code)
+ }
+}
+
+impl From for Error {
+ fn from(err: io::Error) -> Self {
+ sys::io_error_to_apperr(err)
+ }
+}
diff --git a/pkgs/edit/src/arena/debug.rs b/pkgs/edit/src/arena/debug.rs
new file mode 100644
index 0000000..4b1ae6c
--- /dev/null
+++ b/pkgs/edit/src/arena/debug.rs
@@ -0,0 +1,156 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#![allow(clippy::missing_safety_doc, clippy::mut_from_ref)]
+
+use std::alloc::{AllocError, Allocator, Layout};
+use std::mem::{self, MaybeUninit};
+use std::ptr::NonNull;
+
+use super::release;
+use crate::apperr;
+
+/// A debug wrapper for [`release::Arena`].
+///
+/// The problem with [`super::ScratchArena`] is that it only "borrows" an underlying
+/// [`release::Arena`]. Once the [`super::ScratchArena`] is dropped it resets the watermark
+/// of the underlying [`release::Arena`], freeing all allocations done since borrowing it.
+///
+/// It is completely valid for the same [`release::Arena`] to be borrowed multiple times at once,
+/// *as long as* you only use the most recent borrow. Bad example:
+/// ```should_panic
+/// use edit::arena::scratch_arena;
+///
+/// let mut scratch1 = scratch_arena(None);
+/// let mut scratch2 = scratch_arena(None);
+///
+/// let foo = scratch1.alloc_uninit::();
+///
+/// // This will also reset `scratch1`'s allocation.
+/// drop(scratch2);
+///
+/// *foo; // BOOM! ...if it wasn't for our debug wrapper.
+/// ```
+///
+/// To avoid this, this wraps the real [`release::Arena`] in a "debug" one, which pretends as if every
+/// instance of itself is a distinct [`release::Arena`] instance. Then we use this "debug" [`release::Arena`]
+/// for [`super::ScratchArena`] which allows us to track which borrow is the most recent one.
+pub enum Arena {
+ // Delegate is 'static, because release::Arena requires no lifetime
+ // annotations, and so this mere debug helper cannot use them either.
+ Delegated { delegate: &'static release::Arena, borrow: usize },
+ Owned { arena: release::Arena },
+}
+
+impl Drop for Arena {
+ fn drop(&mut self) {
+ if let Arena::Delegated { delegate, borrow } = self {
+ let borrows = delegate.borrows.get();
+ assert_eq!(*borrow, borrows);
+ delegate.borrows.set(borrows - 1);
+ }
+ }
+}
+
+impl Default for Arena {
+ fn default() -> Self {
+ Self::empty()
+ }
+}
+
+impl Arena {
+ pub const fn empty() -> Self {
+ Self::Owned { arena: release::Arena::empty() }
+ }
+
+ pub fn new(capacity: usize) -> apperr::Result {
+ Ok(Self::Owned { arena: release::Arena::new(capacity)? })
+ }
+
+ pub(super) fn delegated(delegate: &release::Arena) -> Arena {
+ let borrow = delegate.borrows.get() + 1;
+ delegate.borrows.set(borrow);
+ Self::Delegated { delegate: unsafe { mem::transmute(delegate) }, borrow }
+ }
+
+ #[inline]
+ pub(super) fn delegate_target(&self) -> &release::Arena {
+ match *self {
+ Arena::Delegated { delegate, borrow } => {
+ assert!(
+ borrow == delegate.borrows.get(),
+ "Arena already borrowed by a newer ScratchArena"
+ );
+ delegate
+ }
+ Arena::Owned { ref arena } => arena,
+ }
+ }
+
+ #[inline]
+ pub(super) fn delegate_target_unchecked(&self) -> &release::Arena {
+ match self {
+ Arena::Delegated { delegate, .. } => delegate,
+ Arena::Owned { arena } => arena,
+ }
+ }
+
+ pub fn offset(&self) -> usize {
+ self.delegate_target().offset()
+ }
+
+ pub unsafe fn reset(&self, to: usize) {
+ unsafe { self.delegate_target().reset(to) }
+ }
+
+ pub fn alloc_uninit(&self) -> &mut MaybeUninit {
+ self.delegate_target().alloc_uninit()
+ }
+
+ pub fn alloc_uninit_slice(&self, count: usize) -> &mut [MaybeUninit] {
+ self.delegate_target().alloc_uninit_slice(count)
+ }
+}
+
+unsafe impl Allocator for Arena {
+ fn allocate(&self, layout: Layout) -> Result, AllocError> {
+ self.delegate_target().alloc_raw(layout.size(), layout.align())
+ }
+
+ fn allocate_zeroed(&self, layout: Layout) -> Result, AllocError> {
+ self.delegate_target().allocate_zeroed(layout)
+ }
+
+ // While it is possible to shrink the tail end of the arena, it is
+ // not very useful given the existence of scoped scratch arenas.
+ unsafe fn deallocate(&self, ptr: NonNull, layout: Layout) {
+ unsafe { self.delegate_target().deallocate(ptr, layout) }
+ }
+
+ unsafe fn grow(
+ &self,
+ ptr: NonNull,
+ old_layout: Layout,
+ new_layout: Layout,
+ ) -> Result, AllocError> {
+ unsafe { self.delegate_target().grow(ptr, old_layout, new_layout) }
+ }
+
+ unsafe fn grow_zeroed(
+ &self,
+ ptr: NonNull,
+ old_layout: Layout,
+ new_layout: Layout,
+ ) -> Result, AllocError> {
+ unsafe { self.delegate_target().grow_zeroed(ptr, old_layout, new_layout) }
+ }
+
+ unsafe fn shrink(
+ &self,
+ ptr: NonNull,
+ old_layout: Layout,
+ new_layout: Layout,
+ ) -> Result, AllocError> {
+ unsafe { self.delegate_target().shrink(ptr, old_layout, new_layout) }
+ }
+}
diff --git a/pkgs/edit/src/arena/mod.rs b/pkgs/edit/src/arena/mod.rs
new file mode 100644
index 0000000..a7099c7
--- /dev/null
+++ b/pkgs/edit/src/arena/mod.rs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! Arena allocators. Small and fast.
+
+#[cfg(debug_assertions)]
+mod debug;
+mod release;
+mod scratch;
+mod string;
+
+#[cfg(all(not(doc), debug_assertions))]
+pub use self::debug::Arena;
+#[cfg(any(doc, not(debug_assertions)))]
+pub use self::release::Arena;
+pub use self::scratch::{ScratchArena, init, scratch_arena};
+pub use self::string::ArenaString;
diff --git a/pkgs/edit/src/arena/release.rs b/pkgs/edit/src/arena/release.rs
new file mode 100644
index 0000000..dec572c
--- /dev/null
+++ b/pkgs/edit/src/arena/release.rs
@@ -0,0 +1,278 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#![allow(clippy::mut_from_ref)]
+
+use std::alloc::{AllocError, Allocator, Layout};
+use std::cell::Cell;
+use std::hint::cold_path;
+use std::mem::MaybeUninit;
+use std::ptr::{self, NonNull};
+use std::{mem, slice};
+
+use crate::helpers::*;
+use crate::{apperr, sys};
+
+const ALLOC_CHUNK_SIZE: usize = 64 * KIBI;
+
+/// An arena allocator.
+///
+/// If you have never used an arena allocator before, think of it as
+/// allocating objects on the stack, but the stack is *really* big.
+/// Each time you allocate, memory gets pushed at the end of the stack,
+/// each time you deallocate, memory gets popped from the end of the stack.
+///
+/// One reason you'd want to use this is obviously performance: It's very simple
+/// and so it's also very fast, >10x faster than your system allocator.
+///
+/// However, modern allocators such as `mimalloc` are just as fast, so why not use them?
+/// Because their performance comes at the cost of binary size and we can't have that.
+///
+/// The biggest benefit though is that it sometimes massively simplifies lifetime
+/// and memory management. This can best be seen by this project's UI code, which
+/// uses an arena to allocate a tree of UI nodes. This is infameously difficult
+/// to do in Rust, but not so when you got an arena allocator:
+/// All nodes have the same lifetime, so you can just use references.
+///
+/// # Safety
+///
+/// **Do not** push objects into the arena that require destructors.
+/// Destructors are not executed. Use a pool allocator for that.
+pub struct Arena {
+ base: NonNull,
+ 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) -> apperr::Result {
+ let capacity = (capacity.max(1) + ALLOC_CHUNK_SIZE - 1) & !(ALLOC_CHUNK_SIZE - 1);
+ let base = unsafe { sys::virtual_reserve(capacity)? };
+
+ Ok(Arena {
+ base,
+ capacity,
+ commit: Cell::new(0),
+ offset: Cell::new(0),
+
+ #[cfg(debug_assertions)]
+ borrows: Cell::new(0),
+ })
+ }
+
+ pub fn offset(&self) -> usize {
+ self.offset.get()
+ }
+
+ /// "Deallocates" the memory in the arena down to the given offset.
+ ///
+ /// # Safety
+ ///
+ /// Obviously, this is GIGA UNSAFE. It runs no destructors and does not check
+ /// whether the offset is valid. You better take care when using this function.
+ pub unsafe fn reset(&self, to: usize) {
+ // Fill the deallocated memory with 0xDD to aid debugging.
+ if cfg!(debug_assertions) && self.offset.get() > to {
+ let commit = self.commit.get();
+ let len = (self.offset.get() + 128).min(commit) - to;
+ unsafe { slice::from_raw_parts_mut(self.base.add(to).as_ptr(), len).fill(0xDD) };
+ }
+
+ self.offset.replace(to);
+ }
+
+ #[inline]
+ pub(super) fn alloc_raw(
+ &self,
+ bytes: usize,
+ alignment: usize,
+ ) -> Result, AllocError> {
+ let commit = self.commit.get();
+ let offset = self.offset.get();
+
+ let beg = (offset + alignment - 1) & !(alignment - 1);
+ let end = beg + bytes;
+
+ if end > commit {
+ return self.alloc_raw_bump(beg, end);
+ }
+
+ if cfg!(debug_assertions) {
+ let ptr = unsafe { self.base.add(offset) };
+ let len = (end + 128).min(self.commit.get()) - offset;
+ unsafe { slice::from_raw_parts_mut(ptr.as_ptr(), len).fill(0xCD) };
+ }
+
+ self.offset.replace(end);
+ Ok(unsafe { NonNull::slice_from_raw_parts(self.base.add(beg), bytes) })
+ }
+
+ // With the code in `alloc_raw_bump()` out of the way, `alloc_raw()` compiles down to some super tight assembly.
+ #[cold]
+ fn alloc_raw_bump(&self, beg: usize, end: usize) -> Result, AllocError> {
+ let offset = self.offset.get();
+ let commit_old = self.commit.get();
+ let commit_new = (end + ALLOC_CHUNK_SIZE - 1) & !(ALLOC_CHUNK_SIZE - 1);
+
+ if commit_new > self.capacity
+ || unsafe {
+ sys::virtual_commit(self.base.add(commit_old), commit_new - commit_old).is_err()
+ }
+ {
+ return Err(AllocError);
+ }
+
+ if cfg!(debug_assertions) {
+ let ptr = unsafe { self.base.add(offset) };
+ let len = (end + 128).min(self.commit.get()) - offset;
+ unsafe { slice::from_raw_parts_mut(ptr.as_ptr(), len).fill(0xCD) };
+ }
+
+ self.commit.replace(commit_new);
+ self.offset.replace(end);
+ Ok(unsafe { NonNull::slice_from_raw_parts(self.base.add(beg), end - beg) })
+ }
+
+ #[allow(clippy::mut_from_ref)]
+ pub fn alloc_uninit(&self) -> &mut MaybeUninit {
+ let bytes = mem::size_of::();
+ let alignment = mem::align_of::();
+ let ptr = self.alloc_raw(bytes, alignment).unwrap();
+ unsafe { ptr.cast().as_mut() }
+ }
+
+ #[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).unwrap();
+ unsafe { slice::from_raw_parts_mut(ptr.cast().as_ptr(), count) }
+ }
+}
+
+impl Drop for Arena {
+ fn drop(&mut self) {
+ if self.base != NonNull::dangling() {
+ unsafe { sys::virtual_release(self.base, self.capacity) };
+ }
+ }
+}
+
+impl Default for Arena {
+ fn default() -> Self {
+ Arena::empty()
+ }
+}
+
+unsafe impl Allocator for Arena {
+ fn allocate(&self, layout: Layout) -> Result, AllocError> {
+ self.alloc_raw(layout.size(), layout.align())
+ }
+
+ fn allocate_zeroed(&self, layout: Layout) -> Result, AllocError> {
+ let p = self.alloc_raw(layout.size(), layout.align())?;
+ unsafe { p.cast::().as_ptr().write_bytes(0, p.len()) }
+ Ok(p)
+ }
+
+ // While it is possible to shrink the tail end of the arena, it is
+ // not very useful given the existence of scoped scratch arenas.
+ unsafe fn deallocate(&self, _: NonNull, _: Layout) {}
+
+ unsafe fn grow(
+ &self,
+ ptr: NonNull,
+ old_layout: Layout,
+ new_layout: Layout,
+ ) -> Result, AllocError> {
+ debug_assert!(new_layout.size() >= old_layout.size());
+ debug_assert!(new_layout.align() <= old_layout.align());
+
+ let new_ptr;
+
+ // Growing the given area is possible if it is at the end of the arena.
+ if unsafe { ptr.add(old_layout.size()) == self.base.add(self.offset.get()) } {
+ new_ptr = ptr;
+ let delta = new_layout.size() - old_layout.size();
+ // Assuming that the given ptr/length area is at the end of the arena,
+ // we can just push more memory to the end of the arena to grow it.
+ self.alloc_raw(delta, 1)?;
+ } else {
+ cold_path();
+
+ new_ptr = self.allocate(new_layout)?.cast();
+
+ // SAFETY: It's weird to me that this doesn't assert new_layout.size() >= old_layout.size(),
+ // but neither does the stdlib code at the time of writing.
+ // So, assuming that is not needed, this code is safe since it just copies the old data over.
+ unsafe {
+ ptr::copy_nonoverlapping(ptr.as_ptr(), new_ptr.as_ptr(), old_layout.size());
+ self.deallocate(ptr, old_layout);
+ }
+ }
+
+ Ok(NonNull::slice_from_raw_parts(new_ptr, new_layout.size()))
+ }
+
+ unsafe fn grow_zeroed(
+ &self,
+ ptr: NonNull,
+ old_layout: Layout,
+ new_layout: Layout,
+ ) -> Result, AllocError> {
+ unsafe {
+ // SAFETY: Same as grow().
+ let ptr = self.grow(ptr, old_layout, new_layout)?;
+
+ // SAFETY: At this point, `ptr` must be valid for `new_layout.size()` bytes,
+ // allowing us to safely zero out the delta since `old_layout.size()`.
+ ptr.cast::()
+ .add(old_layout.size())
+ .write_bytes(0, new_layout.size() - old_layout.size());
+
+ Ok(ptr)
+ }
+ }
+
+ unsafe fn shrink(
+ &self,
+ ptr: NonNull,
+ old_layout: Layout,
+ new_layout: Layout,
+ ) -> Result, AllocError> {
+ debug_assert!(new_layout.size() <= old_layout.size());
+ debug_assert!(new_layout.align() <= old_layout.align());
+
+ let mut len = old_layout.size();
+
+ // Shrinking the given area is possible if it is at the end of the arena.
+ if unsafe { ptr.add(len) == self.base.add(self.offset.get()) } {
+ self.offset.set(self.offset.get() - len + new_layout.size());
+ len = new_layout.size();
+ } else {
+ debug_assert!(
+ false,
+ "Did you call shrink_to_fit()? Only the last allocation can be shrunk!"
+ );
+ }
+
+ Ok(NonNull::slice_from_raw_parts(ptr, len))
+ }
+}
diff --git a/pkgs/edit/src/arena/scratch.rs b/pkgs/edit/src/arena/scratch.rs
new file mode 100644
index 0000000..a719db4
--- /dev/null
+++ b/pkgs/edit/src/arena/scratch.rs
@@ -0,0 +1,112 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use std::ops::Deref;
+
+#[cfg(debug_assertions)]
+use super::debug;
+use super::{Arena, release};
+use crate::apperr;
+use crate::helpers::*;
+
+static mut S_SCRATCH: [release::Arena; 2] =
+ const { [release::Arena::empty(), release::Arena::empty()] };
+
+/// Initialize the scratch arenas with a given capacity.
+/// Call this before using [`scratch_arena`].
+pub fn init(capacity: usize) -> apperr::Result<()> {
+ unsafe {
+ for s in &mut S_SCRATCH[..] {
+ *s = release::Arena::new(capacity)?;
+ }
+ }
+ Ok(())
+}
+
+/// Need an arena for temporary allocations? [`scratch_arena`] got you covered.
+/// Call [`scratch_arena`] and it'll return an [`Arena`] that resets when it goes out of scope.
+///
+/// ---
+///
+/// Most methods make just two kinds of allocations:
+/// * Interior: Temporary data that can be deallocated when the function returns.
+/// * Exterior: Data that is returned to the caller and must remain alive until the caller stops using it.
+///
+/// Such methods only have two lifetimes, for which you consequently also only need two arenas.
+/// ...even if your method calls other methods recursively! This is because the exterior allocations
+/// of a callee are simply interior allocations to the caller, and so on, recursively.
+///
+/// This works as long as the two arenas flip/flop between being used as interior/exterior allocator
+/// along the callstack. To ensure that is the case, we use a recursion counter in debug builds.
+///
+/// This approach was described among others at:
+///
+/// # Safety
+///
+/// If your function takes an [`Arena`] argument, you **MUST** pass it to `scratch_arena` as `Some(&arena)`.
+pub fn scratch_arena(conflict: Option<&Arena>) -> ScratchArena<'static> {
+ unsafe {
+ #[cfg(debug_assertions)]
+ let conflict = conflict.map(|a| a.delegate_target_unchecked());
+
+ let index = opt_ptr_eq(conflict, Some(&S_SCRATCH[0])) as usize;
+ let arena = &mut S_SCRATCH[index];
+ ScratchArena::new(arena)
+ }
+}
+
+/// Borrows an [`Arena`] for temporary allocations.
+///
+/// See [`scratch_arena`].
+#[cfg(debug_assertions)]
+pub struct ScratchArena<'a> {
+ arena: debug::Arena,
+ offset: usize,
+ _phantom: std::marker::PhantomData<&'a ()>,
+}
+
+#[cfg(not(debug_assertions))]
+pub struct ScratchArena<'a> {
+ arena: &'a Arena,
+ offset: usize,
+}
+
+#[cfg(debug_assertions)]
+impl<'a> ScratchArena<'a> {
+ fn new(arena: &'a release::Arena) -> Self {
+ let offset = arena.offset();
+ ScratchArena { arena: Arena::delegated(arena), _phantom: std::marker::PhantomData, offset }
+ }
+}
+
+#[cfg(not(debug_assertions))]
+impl<'a> ScratchArena<'a> {
+ fn new(arena: &'a release::Arena) -> Self {
+ let offset = arena.offset();
+ ScratchArena { arena, offset }
+ }
+}
+
+impl Drop for ScratchArena<'_> {
+ fn drop(&mut self) {
+ unsafe { self.arena.reset(self.offset) };
+ }
+}
+
+#[cfg(debug_assertions)]
+impl Deref for ScratchArena<'_> {
+ type Target = debug::Arena;
+
+ fn deref(&self) -> &Self::Target {
+ &self.arena
+ }
+}
+
+#[cfg(not(debug_assertions))]
+impl Deref for ScratchArena<'_> {
+ type Target = Arena;
+
+ fn deref(&self) -> &Self::Target {
+ self.arena
+ }
+}
diff --git a/pkgs/edit/src/arena/string.rs b/pkgs/edit/src/arena/string.rs
new file mode 100644
index 0000000..ff70ec1
--- /dev/null
+++ b/pkgs/edit/src/arena/string.rs
@@ -0,0 +1,281 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use std::fmt;
+use std::ops::{Bound, Deref, DerefMut, RangeBounds};
+
+use super::Arena;
+use crate::helpers::*;
+
+/// A custom string type, because `std` lacks allocator support for [`String`].
+///
+/// To keep things simple, this one is hardcoded to [`Arena`].
+#[derive(Clone)]
+pub struct ArenaString<'a> {
+ vec: Vec,
+}
+
+impl<'a> ArenaString<'a> {
+ /// Creates a new [`ArenaString`] in the given arena.
+ #[must_use]
+ pub const fn new_in(arena: &'a Arena) -> Self {
+ Self { vec: Vec::new_in(arena) }
+ }
+
+ #[must_use]
+ pub fn with_capacity_in(capacity: usize, arena: &'a Arena) -> Self {
+ Self { vec: Vec::with_capacity_in(capacity, arena) }
+ }
+
+ /// Turns a [`str`] into an [`ArenaString`].
+ #[must_use]
+ pub fn from_str(arena: &'a Arena, s: &str) -> Self {
+ let mut res = Self::new_in(arena);
+ res.push_str(s);
+ res
+ }
+
+ /// It says right here that you checked if `bytes` is valid UTF-8
+ /// and you are sure it is. Presto! Here's an `ArenaString`!
+ ///
+ /// # Safety
+ ///
+ /// You fool! It says "unchecked" right there. Now the house is burning.
+ #[inline]
+ #[must_use]
+ pub unsafe fn from_utf8_unchecked(bytes: Vec) -> Self {
+ Self { vec: bytes }
+ }
+
+ /// Checks whether `text` contains only valid UTF-8.
+ /// If the entire string is valid, it returns `Ok(text)`.
+ /// Otherwise, it returns `Err(ArenaString)` with all invalid sequences replaced with U+FFFD.
+ pub fn from_utf8_lossy<'s>(
+ arena: &'a Arena,
+ text: &'s [u8],
+ ) -> Result<&'s str, ArenaString<'a>> {
+ let mut iter = text.utf8_chunks();
+ let Some(mut chunk) = iter.next() else {
+ return Ok("");
+ };
+
+ let valid = chunk.valid();
+ if chunk.invalid().is_empty() {
+ debug_assert_eq!(valid.len(), text.len());
+ return Ok(unsafe { str::from_utf8_unchecked(text) });
+ }
+
+ const REPLACEMENT: &str = "\u{FFFD}";
+
+ let mut res = Self::new_in(arena);
+ res.reserve(text.len());
+
+ loop {
+ res.push_str(chunk.valid());
+ if !chunk.invalid().is_empty() {
+ res.push_str(REPLACEMENT);
+ }
+ chunk = match iter.next() {
+ Some(chunk) => chunk,
+ None => break,
+ };
+ }
+
+ Err(res)
+ }
+
+ /// Turns a [`Vec`] into an [`ArenaString`], replacing invalid UTF-8 sequences with U+FFFD.
+ #[must_use]
+ pub fn from_utf8_lossy_owned(v: Vec) -> Self {
+ match Self::from_utf8_lossy(v.allocator(), &v) {
+ Ok(..) => unsafe { Self::from_utf8_unchecked(v) },
+ Err(s) => s,
+ }
+ }
+
+ /// It's empty.
+ pub fn is_empty(&self) -> bool {
+ self.vec.is_empty()
+ }
+
+ /// It's lengthy.
+ pub fn len(&self) -> usize {
+ self.vec.len()
+ }
+
+ /// It's capacatity.
+ pub fn capacity(&self) -> usize {
+ self.vec.capacity()
+ }
+
+ /// It's a [`String`], now it's a [`str`]. Wow!
+ pub fn as_str(&self) -> &str {
+ unsafe { str::from_utf8_unchecked(self.vec.as_slice()) }
+ }
+
+ /// It's a [`String`], now it's a [`str`]. And it's mutable! WOW!
+ pub fn as_mut_str(&mut self) -> &mut str {
+ unsafe { str::from_utf8_unchecked_mut(self.vec.as_mut_slice()) }
+ }
+
+ /// Now it's bytes!
+ pub fn as_bytes(&self) -> &[u8] {
+ self.vec.as_slice()
+ }
+
+ /// Returns a mutable reference to the contents of this `String`.
+ ///
+ /// # Safety
+ ///
+ /// The underlying `&mut Vec` allows writing bytes which are not valid UTF-8.
+ pub unsafe fn as_mut_vec(&mut self) -> &mut Vec {
+ &mut self.vec
+ }
+
+ /// Reserves *additional* memory. For you old folks out there (totally not me),
+ /// this is differrent from C++'s `reserve` which reserves a total size.
+ pub fn reserve(&mut self, additional: usize) {
+ self.vec.reserve(additional)
+ }
+
+ /// Just like [`ArenaString::reserve`], but it doesn't overallocate.
+ pub fn reserve_exact(&mut self, additional: usize) {
+ self.vec.reserve_exact(additional)
+ }
+
+ /// Now it's small! Alarming!
+ ///
+ /// *Do not* call this unless this string is the last thing on the arena.
+ /// Arenas are stacks, they can't deallocate what's in the middle.
+ pub fn shrink_to_fit(&mut self) {
+ self.vec.shrink_to_fit()
+ }
+
+ /// To no surprise, this clears the string.
+ pub fn clear(&mut self) {
+ self.vec.clear()
+ }
+
+ /// Append some text.
+ pub fn push_str(&mut self, string: &str) {
+ self.vec.extend_from_slice(string.as_bytes())
+ }
+
+ /// Append a single character.
+ #[inline]
+ pub fn push(&mut self, ch: char) {
+ match ch.len_utf8() {
+ 1 => self.vec.push(ch as u8),
+ _ => self.vec.extend_from_slice(ch.encode_utf8(&mut [0; 4]).as_bytes()),
+ }
+ }
+
+ /// Same as `push(char)` but with a specified number of character copies.
+ /// Shockingly absent from the standard library.
+ pub fn push_repeat(&mut self, ch: char, total_copies: usize) {
+ if total_copies == 0 {
+ return;
+ }
+
+ let buf = unsafe { self.as_mut_vec() };
+
+ if ch.is_ascii() {
+ // Compiles down to `memset()`.
+ buf.extend(std::iter::repeat_n(ch as u8, total_copies));
+ } else {
+ // Implements efficient string padding using quadratic duplication.
+ let mut utf8_buf = [0; 4];
+ let utf8 = ch.encode_utf8(&mut utf8_buf).as_bytes();
+ let initial_len = buf.len();
+ let added_len = utf8.len() * total_copies;
+ let final_len = initial_len + added_len;
+
+ buf.reserve(added_len);
+ buf.extend_from_slice(utf8);
+
+ while buf.len() != final_len {
+ let end = (final_len - buf.len() + initial_len).min(buf.len());
+ buf.extend_from_within(initial_len..end);
+ }
+ }
+ }
+
+ /// Replaces a range of characters with a new string.
+ pub fn replace_range>(&mut self, range: R, replace_with: &str) {
+ match range.start_bound() {
+ Bound::Included(&n) => assert!(self.is_char_boundary(n)),
+ Bound::Excluded(&n) => assert!(self.is_char_boundary(n + 1)),
+ Bound::Unbounded => {}
+ };
+ match range.end_bound() {
+ Bound::Included(&n) => assert!(self.is_char_boundary(n + 1)),
+ Bound::Excluded(&n) => assert!(self.is_char_boundary(n)),
+ Bound::Unbounded => {}
+ };
+ unsafe { self.as_mut_vec() }.replace_range(range, replace_with.as_bytes());
+ }
+
+ /// Finds `old` in the string and replaces it with `new`.
+ /// Only performs one replacement.
+ pub fn replace_once_in_place(&mut self, old: &str, new: &str) {
+ if let Some(beg) = self.find(old) {
+ unsafe { self.as_mut_vec() }.replace_range(beg..beg + old.len(), new.as_bytes());
+ }
+ }
+}
+
+impl fmt::Debug for ArenaString<'_> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ fmt::Debug::fmt(&**self, f)
+ }
+}
+
+impl PartialEq<&str> for ArenaString<'_> {
+ fn eq(&self, other: &&str) -> bool {
+ self.as_str() == *other
+ }
+}
+
+impl Deref for ArenaString<'_> {
+ type Target = str;
+
+ fn deref(&self) -> &Self::Target {
+ self.as_str()
+ }
+}
+
+impl DerefMut for ArenaString<'_> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ self.as_mut_str()
+ }
+}
+
+impl fmt::Display for ArenaString<'_> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(self.as_str())
+ }
+}
+
+impl fmt::Write for ArenaString<'_> {
+ #[inline]
+ fn write_str(&mut self, s: &str) -> fmt::Result {
+ self.push_str(s);
+ Ok(())
+ }
+
+ #[inline]
+ fn write_char(&mut self, c: char) -> fmt::Result {
+ self.push(c);
+ Ok(())
+ }
+}
+
+#[macro_export]
+macro_rules! arena_format {
+ ($arena:expr, $($arg:tt)*) => {{
+ use std::fmt::Write as _;
+ let mut output = $crate::arena::ArenaString::new_in($arena);
+ output.write_fmt(format_args!($($arg)*)).unwrap();
+ output
+ }}
+}
diff --git a/pkgs/edit/src/base64.rs b/pkgs/edit/src/base64.rs
new file mode 100644
index 0000000..bce13c6
--- /dev/null
+++ b/pkgs/edit/src/base64.rs
@@ -0,0 +1,121 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! Base64 facilities.
+
+use crate::arena::ArenaString;
+
+const CHARSET: [u8; 64] = *b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+/// One aspect of base64 is that the encoded length can be
+/// calculated accurately in advance, which is what this returns.
+#[inline]
+pub fn encode_len(src_len: usize) -> usize {
+ src_len.div_ceil(3) * 4
+}
+
+/// Encodes the given bytes as base64 and appends them to the destination string.
+pub fn encode(dst: &mut ArenaString, src: &[u8]) {
+ unsafe {
+ let mut inp = src.as_ptr();
+ let mut remaining = src.len();
+ let dst = dst.as_mut_vec();
+
+ let out_len = encode_len(src.len());
+ // ... we can then use this fact to reserve space all at once.
+ dst.reserve(out_len);
+
+ // SAFETY: Getting a pointer to the reserved space is only safe
+ // *after* calling `reserve()` as it may change the pointer.
+ let mut out = dst.as_mut_ptr().add(dst.len());
+
+ if remaining != 0 {
+ // Translate chunks of 3 source bytes into 4 base64-encoded bytes.
+ while remaining > 3 {
+ // SAFETY: Thanks to `remaining > 3`, reading 4 bytes at once is safe.
+ // This improves performance massively over a byte-by-byte approach,
+ // because it allows us to byte-swap the read and use simple bit-shifts below.
+ let val = u32::from_be((inp as *const u32).read_unaligned());
+ inp = inp.add(3);
+ remaining -= 3;
+
+ *out = CHARSET[(val >> 26) as usize];
+ out = out.add(1);
+ *out = CHARSET[(val >> 20) as usize & 0x3f];
+ out = out.add(1);
+ *out = CHARSET[(val >> 14) as usize & 0x3f];
+ out = out.add(1);
+ *out = CHARSET[(val >> 8) as usize & 0x3f];
+ out = out.add(1);
+ }
+
+ // Convert the remaining 1-3 bytes.
+ let mut in1 = 0;
+ let mut in2 = 0;
+
+ // We can simplify the following logic by assuming that there's only 1
+ // byte left. If there's >1 byte left, these two '=' will be overwritten.
+ *out.add(3) = b'=';
+ *out.add(2) = b'=';
+
+ if remaining >= 3 {
+ in2 = inp.add(2).read() as usize;
+ *out.add(3) = CHARSET[in2 & 0x3f];
+ }
+
+ if remaining >= 2 {
+ in1 = inp.add(1).read() as usize;
+ *out.add(2) = CHARSET[(in1 << 2 | in2 >> 6) & 0x3f];
+ }
+
+ let in0 = inp.add(0).read() as usize;
+ *out.add(1) = CHARSET[(in0 << 4 | in1 >> 4) & 0x3f];
+ *out.add(0) = CHARSET[in0 >> 2];
+ }
+
+ dst.set_len(dst.len() + out_len);
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::encode;
+ use crate::arena::{Arena, ArenaString};
+
+ #[test]
+ fn test_basic() {
+ let arena = Arena::new(4 * 1024).unwrap();
+ let enc = |s: &[u8]| {
+ let mut dst = ArenaString::new_in(&arena);
+ encode(&mut dst, s);
+ dst
+ };
+ assert_eq!(enc(b""), "");
+ assert_eq!(enc(b"a"), "YQ==");
+ assert_eq!(enc(b"ab"), "YWI=");
+ assert_eq!(enc(b"abc"), "YWJj");
+ assert_eq!(enc(b"abcd"), "YWJjZA==");
+ assert_eq!(enc(b"abcde"), "YWJjZGU=");
+ assert_eq!(enc(b"abcdef"), "YWJjZGVm");
+ assert_eq!(enc(b"abcdefg"), "YWJjZGVmZw==");
+ assert_eq!(enc(b"abcdefgh"), "YWJjZGVmZ2g=");
+ assert_eq!(enc(b"abcdefghi"), "YWJjZGVmZ2hp");
+ assert_eq!(enc(b"abcdefghij"), "YWJjZGVmZ2hpag==");
+ assert_eq!(enc(b"abcdefghijk"), "YWJjZGVmZ2hpams=");
+ assert_eq!(enc(b"abcdefghijkl"), "YWJjZGVmZ2hpamts");
+ assert_eq!(enc(b"abcdefghijklm"), "YWJjZGVmZ2hpamtsbQ==");
+ assert_eq!(enc(b"abcdefghijklmN"), "YWJjZGVmZ2hpamtsbU4=");
+ assert_eq!(enc(b"abcdefghijklmNO"), "YWJjZGVmZ2hpamtsbU5P");
+ assert_eq!(enc(b"abcdefghijklmNOP"), "YWJjZGVmZ2hpamtsbU5PUA==");
+ assert_eq!(enc(b"abcdefghijklmNOPQ"), "YWJjZGVmZ2hpamtsbU5PUFE=");
+ assert_eq!(enc(b"abcdefghijklmNOPQR"), "YWJjZGVmZ2hpamtsbU5PUFFS");
+ assert_eq!(enc(b"abcdefghijklmNOPQRS"), "YWJjZGVmZ2hpamtsbU5PUFFSUw==");
+ assert_eq!(enc(b"abcdefghijklmNOPQRST"), "YWJjZGVmZ2hpamtsbU5PUFFSU1Q=");
+ assert_eq!(enc(b"abcdefghijklmNOPQRSTU"), "YWJjZGVmZ2hpamtsbU5PUFFSU1RV");
+ assert_eq!(enc(b"abcdefghijklmNOPQRSTUV"), "YWJjZGVmZ2hpamtsbU5PUFFSU1RVVg==");
+ assert_eq!(enc(b"abcdefghijklmNOPQRSTUVW"), "YWJjZGVmZ2hpamtsbU5PUFFSU1RVVlc=");
+ assert_eq!(enc(b"abcdefghijklmNOPQRSTUVWX"), "YWJjZGVmZ2hpamtsbU5PUFFSU1RVVldY");
+ assert_eq!(enc(b"abcdefghijklmNOPQRSTUVWXY"), "YWJjZGVmZ2hpamtsbU5PUFFSU1RVVldYWQ==");
+ assert_eq!(enc(b"abcdefghijklmNOPQRSTUVWXYZ"), "YWJjZGVmZ2hpamtsbU5PUFFSU1RVVldYWVo=");
+ }
+}
diff --git a/pkgs/edit/src/bin/edit/documents.rs b/pkgs/edit/src/bin/edit/documents.rs
new file mode 100644
index 0000000..349f4f3
--- /dev/null
+++ b/pkgs/edit/src/bin/edit/documents.rs
@@ -0,0 +1,296 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use std::collections::LinkedList;
+use std::ffi::OsStr;
+use std::fs::File;
+use std::path::{Path, PathBuf};
+
+use edit::buffer::{RcTextBuffer, TextBuffer};
+use edit::helpers::{CoordType, Point};
+use edit::simd::memrchr2;
+use edit::{apperr, path, sys};
+
+use crate::state::DisplayablePathBuf;
+
+pub struct Document {
+ pub buffer: RcTextBuffer,
+ pub path: Option,
+ 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(&file) {
+ self.file_id = Some(id);
+ }
+
+ if let Some(path) = new_path {
+ self.set_path(path);
+ }
+
+ Ok(())
+ }
+
+ pub fn reread(&mut self, encoding: Option<&'static str>) -> apperr::Result<()> {
+ let path = self.path.as_ref().unwrap().as_path();
+ let mut file = DocumentManager::open_for_reading(path)?;
+
+ {
+ let mut tb = self.buffer.borrow_mut();
+ tb.read_file(&mut file, encoding)?;
+ }
+
+ if let Ok(id) = sys::file_id(&file) {
+ self.file_id = Some(id);
+ }
+
+ Ok(())
+ }
+
+ fn set_path(&mut self, path: PathBuf) {
+ let filename = path.file_name().unwrap_or_default().to_string_lossy().into_owned();
+ let dir = path.parent().map(ToOwned::to_owned).unwrap_or_default();
+ self.filename = filename;
+ self.dir = Some(DisplayablePathBuf::new(dir));
+ self.path = Some(path);
+ self.update_file_mode();
+ }
+
+ fn update_file_mode(&mut self) {
+ let mut tb = self.buffer.borrow_mut();
+ tb.set_ruler(if self.filename == "COMMIT_EDITMSG" { 72 } else { 0 });
+ }
+}
+
+#[derive(Default)]
+pub struct DocumentManager {
+ list: LinkedList,
+}
+
+impl DocumentManager {
+ #[inline]
+ pub fn len(&self) -> usize {
+ self.list.len()
+ }
+
+ #[inline]
+ pub fn active(&self) -> Option<&Document> {
+ self.list.front()
+ }
+
+ #[inline]
+ pub fn active_mut(&mut self) -> Option<&mut Document> {
+ self.list.front_mut()
+ }
+
+ #[inline]
+ pub fn update_active bool>(&mut self, mut func: F) -> bool {
+ let mut cursor = self.list.cursor_front_mut();
+ while let Some(doc) = cursor.current() {
+ if func(doc) {
+ let list = cursor.remove_current_as_list().unwrap();
+ self.list.cursor_front_mut().splice_before(list);
+ return true;
+ }
+ cursor.move_next();
+ }
+ false
+ }
+
+ pub fn remove_active(&mut self) {
+ self.list.pop_front();
+ }
+
+ pub fn add_untitled(&mut self) -> apperr::Result<&mut Document> {
+ let buffer = TextBuffer::new_rc(false)?;
+ {
+ let mut tb = buffer.borrow_mut();
+ tb.set_margin_enabled(true);
+ tb.set_line_highlight_enabled(true);
+ }
+
+ let mut doc = Document {
+ buffer,
+ path: None,
+ dir: Default::default(),
+ filename: Default::default(),
+ file_id: None,
+ new_file_counter: 0,
+ };
+ self.gen_untitled_name(&mut doc);
+
+ self.list.push_front(doc);
+ Ok(self.list.front_mut().unwrap())
+ }
+
+ pub fn gen_untitled_name(&self, doc: &mut Document) {
+ let mut new_file_counter = 0;
+ for doc in &self.list {
+ new_file_counter = new_file_counter.max(doc.new_file_counter);
+ }
+ new_file_counter += 1;
+
+ doc.filename = format!("Untitled-{new_file_counter}.txt");
+ doc.new_file_counter = new_file_counter;
+ }
+
+ pub fn add_file_path(&mut self, path: &Path) -> apperr::Result<&mut Document> {
+ let (path, goto) = Self::parse_filename_goto(path);
+ let path = path::normalize(path);
+
+ let mut file = match Self::open_for_reading(&path) {
+ Ok(file) => Some(file),
+ Err(err) if sys::apperr_is_not_found(err) => None,
+ Err(err) => return Err(err),
+ };
+
+ let file_id = match &file {
+ Some(file) => Some(sys::file_id(file)?),
+ None => None,
+ };
+
+ // Check if the file is already open.
+ if file_id.is_some() && self.update_active(|doc| doc.file_id == file_id) {
+ let doc = self.active_mut().unwrap();
+ if let Some(goto) = goto {
+ doc.buffer.borrow_mut().cursor_move_to_logical(goto);
+ }
+ return Ok(doc);
+ }
+
+ let buffer = TextBuffer::new_rc(false)?;
+ {
+ let mut tb = buffer.borrow_mut();
+ tb.set_margin_enabled(true);
+ tb.set_line_highlight_enabled(true);
+
+ if let Some(file) = &mut file {
+ tb.read_file(file, None)?;
+
+ if let Some(goto) = goto
+ && goto != Default::default()
+ {
+ tb.cursor_move_to_logical(goto);
+ }
+ }
+ }
+
+ let mut doc = Document {
+ buffer,
+ path: None,
+ dir: None,
+ filename: Default::default(),
+ file_id,
+ new_file_counter: 0,
+ };
+ doc.set_path(path);
+
+ self.list.push_front(doc);
+ Ok(self.list.front_mut().unwrap())
+ }
+
+ pub fn open_for_reading(path: &Path) -> apperr::Result {
+ File::open(path).map_err(apperr::Error::from)
+ }
+
+ pub fn open_for_writing(path: &Path) -> apperr::Result {
+ File::create(path).map_err(apperr::Error::from)
+ }
+
+ // Parse a filename in the form of "filename:line:char".
+ // Returns the position of the first colon and the line/char coordinates.
+ fn parse_filename_goto(path: &Path) -> (&Path, Option) {
+ 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)
+ }
+
+ let bytes = path.as_os_str().as_encoded_bytes();
+ let colend = match memrchr2(b':', b':', bytes, bytes.len()) {
+ // Reject filenames that would result in an empty filename after stripping off the :line:char suffix.
+ // For instance, a filename like ":123:456" will not be processed by this function.
+ Some(colend) if colend > 0 => colend,
+ _ => return (path, None),
+ };
+
+ let last = match parse(&bytes[colend + 1..]) {
+ Some(last) => last,
+ None => return (path, None),
+ };
+ let last = (last - 1).max(0);
+ let mut len = colend;
+ let mut goto = Point { x: 0, y: last };
+
+ if let Some(colbeg) = memrchr2(b':', b':', bytes, colend) {
+ // Same here: Don't allow empty filenames.
+ if colbeg != 0
+ && let Some(first) = parse(&bytes[colbeg + 1..colend])
+ {
+ let first = (first - 1).max(0);
+ len = colbeg;
+ goto = Point { x: last, y: first };
+ }
+ }
+
+ // Strip off the :line:char suffix.
+ let path = &bytes[..len];
+ let path = unsafe { OsStr::from_encoded_bytes_unchecked(path) };
+ let path = Path::new(path);
+ (path, Some(goto))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_parse_last_numbers() {
+ fn parse(s: &str) -> (&str, Option) {
+ 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 })));
+ }
+}
diff --git a/pkgs/edit/src/bin/edit/draw_editor.rs b/pkgs/edit/src/bin/edit/draw_editor.rs
new file mode 100644
index 0000000..bdab25a
--- /dev/null
+++ b/pkgs/edit/src/bin/edit/draw_editor.rs
@@ -0,0 +1,273 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use edit::framebuffer::IndexedColor;
+use edit::helpers::*;
+use edit::icu;
+use edit::input::{kbmod, vk};
+use edit::tui::*;
+
+use crate::localization::*;
+use crate::state::*;
+
+pub fn draw_editor(ctx: &mut Context, state: &mut State) {
+ if !matches!(state.wants_search.kind, StateSearchKind::Hidden | StateSearchKind::Disabled) {
+ draw_search(ctx, state);
+ }
+
+ let size = ctx.size();
+ // TODO: The layout code should be able to just figure out the height on its own.
+ let height_reduction = match state.wants_search.kind {
+ StateSearchKind::Search => 4,
+ StateSearchKind::Replace => 5,
+ _ => 2,
+ };
+
+ if let Some(doc) = state.documents.active() {
+ ctx.textarea("textarea", doc.buffer.clone());
+ ctx.inherit_focus();
+ } else {
+ ctx.block_begin("empty");
+ ctx.block_end();
+ }
+
+ ctx.attr_intrinsic_size(Size { width: 0, height: size.height - height_reduction });
+}
+
+fn draw_search(ctx: &mut Context, state: &mut State) {
+ enum SearchAction {
+ None,
+ Search,
+ Replace,
+ ReplaceAll,
+ }
+
+ if let Err(err) = icu::init() {
+ error_log_add(ctx, state, err);
+ state.wants_search.kind = StateSearchKind::Disabled;
+ return;
+ }
+
+ let Some(doc) = state.documents.active() else {
+ state.wants_search.kind = StateSearchKind::Hidden;
+ return;
+ };
+
+ let mut action = SearchAction::None;
+ let mut focus = StateSearchKind::Hidden;
+
+ if state.wants_search.focus {
+ state.wants_search.focus = false;
+ focus = StateSearchKind::Search;
+
+ // If the selection is empty, focus the search input field.
+ // Otherwise, focus the replace input field, if it exists.
+ if let Some(selection) = doc.buffer.borrow_mut().extract_user_selection(false) {
+ state.search_needle = String::from_utf8_lossy_owned(selection);
+ focus = state.wants_search.kind;
+ }
+ }
+
+ ctx.block_begin("search");
+ ctx.attr_focus_well();
+ ctx.attr_background_rgba(ctx.indexed(IndexedColor::White));
+ ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::Black));
+ {
+ if ctx.contains_focus() && ctx.consume_shortcut(vk::ESCAPE) {
+ state.wants_search.kind = StateSearchKind::Hidden;
+ }
+
+ ctx.table_begin("needle");
+ ctx.table_set_cell_gap(Size { width: 1, height: 0 });
+ {
+ {
+ ctx.table_next_row();
+ ctx.label("label", loc(LocId::SearchNeedleLabel));
+
+ if ctx.editline("needle", &mut state.search_needle) {
+ action = SearchAction::Search;
+ }
+ if !state.search_success {
+ ctx.attr_background_rgba(ctx.indexed(IndexedColor::Red));
+ ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::BrightWhite));
+ }
+ ctx.attr_intrinsic_size(Size { width: COORD_TYPE_SAFE_MAX, height: 1 });
+ if focus == StateSearchKind::Search {
+ ctx.steal_focus();
+ }
+ if ctx.is_focused() && ctx.consume_shortcut(vk::RETURN) {
+ action = SearchAction::Search;
+ }
+ }
+
+ if state.wants_search.kind == StateSearchKind::Replace {
+ ctx.table_next_row();
+ ctx.label("label", loc(LocId::SearchReplacementLabel));
+
+ ctx.editline("replacement", &mut state.search_replacement);
+ ctx.attr_intrinsic_size(Size { width: COORD_TYPE_SAFE_MAX, height: 1 });
+ if focus == StateSearchKind::Replace {
+ ctx.steal_focus();
+ }
+ if ctx.is_focused() {
+ if ctx.consume_shortcut(vk::RETURN) {
+ action = SearchAction::Replace;
+ } else if ctx.consume_shortcut(kbmod::CTRL_ALT | vk::RETURN) {
+ action = SearchAction::ReplaceAll;
+ }
+ }
+ }
+ }
+ ctx.table_end();
+
+ ctx.table_begin("options");
+ ctx.table_set_cell_gap(Size { width: 2, height: 0 });
+ {
+ ctx.table_next_row();
+
+ let mut change = false;
+ change |= ctx.checkbox(
+ "match-case",
+ loc(LocId::SearchMatchCase),
+ &mut state.search_options.match_case,
+ );
+ change |= ctx.checkbox(
+ "whole-word",
+ loc(LocId::SearchWholeWord),
+ &mut state.search_options.whole_word,
+ );
+ change |= ctx.checkbox(
+ "use-regex",
+ loc(LocId::SearchUseRegex),
+ &mut state.search_options.use_regex,
+ );
+ if change {
+ action = SearchAction::Search;
+ state.wants_search.focus = true;
+ ctx.needs_rerender();
+ }
+
+ if state.wants_search.kind == StateSearchKind::Replace
+ && ctx.button("replace-all", loc(LocId::SearchReplaceAll))
+ {
+ action = SearchAction::ReplaceAll;
+ }
+
+ if ctx.button("close", loc(LocId::SearchClose)) {
+ state.wants_search.kind = StateSearchKind::Hidden;
+ }
+ }
+ ctx.table_end();
+ }
+ ctx.block_end();
+
+ state.search_success = match action {
+ SearchAction::None => return,
+ SearchAction::Search => {
+ doc.buffer.borrow_mut().find_and_select(&state.search_needle, state.search_options)
+ }
+ SearchAction::Replace => doc.buffer.borrow_mut().find_and_replace(
+ &state.search_needle,
+ state.search_options,
+ &state.search_replacement,
+ ),
+ SearchAction::ReplaceAll => doc.buffer.borrow_mut().find_and_replace_all(
+ &state.search_needle,
+ state.search_options,
+ &state.search_replacement,
+ ),
+ }
+ .is_ok();
+
+ ctx.needs_rerender();
+}
+
+pub fn draw_handle_save(ctx: &mut Context, state: &mut State) {
+ if let Some(doc) = state.documents.active_mut() {
+ if doc.path.is_some() {
+ if let Err(err) = doc.save(None) {
+ error_log_add(ctx, state, err);
+ }
+ } else {
+ // No path? Show the file picker.
+ state.wants_file_picker = StateFilePicker::SaveAs;
+ state.wants_save = false;
+ ctx.needs_rerender();
+ }
+ }
+
+ state.wants_save = false;
+}
+
+pub fn draw_handle_wants_close(ctx: &mut Context, state: &mut State) {
+ let Some(doc) = state.documents.active() else {
+ state.wants_close = false;
+ state.wants_exit = true;
+ return;
+ };
+
+ if !doc.buffer.borrow().is_dirty() {
+ state.documents.remove_active();
+ state.wants_close = false;
+ ctx.needs_rerender();
+ return;
+ }
+
+ enum Action {
+ None,
+ Save,
+ Discard,
+ Cancel,
+ }
+ let mut action = Action::None;
+
+ ctx.modal_begin("unsaved-changes", loc(LocId::UnsavedChangesDialogTitle));
+ ctx.attr_background_rgba(ctx.indexed(IndexedColor::Red));
+ ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::BrightWhite));
+ {
+ ctx.label("description", loc(LocId::UnsavedChangesDialogDescription));
+ ctx.attr_padding(Rect::three(1, 2, 1));
+
+ ctx.table_begin("choices");
+ ctx.inherit_focus();
+ ctx.attr_padding(Rect::three(0, 2, 1));
+ ctx.attr_position(Position::Center);
+ ctx.table_set_cell_gap(Size { width: 2, height: 0 });
+ {
+ ctx.table_next_row();
+ ctx.inherit_focus();
+
+ if ctx.button("yes", loc(LocId::UnsavedChangesDialogYes)) {
+ action = Action::Save;
+ }
+ ctx.inherit_focus();
+ if ctx.button("no", loc(LocId::UnsavedChangesDialogNo)) {
+ action = Action::Discard;
+ }
+ if ctx.button("cancel", loc(LocId::Cancel)) {
+ action = Action::Cancel;
+ }
+
+ // TODO: This should highlight the corresponding letter in the label.
+ if ctx.consume_shortcut(vk::S) {
+ action = Action::Save;
+ } else if ctx.consume_shortcut(vk::N) {
+ action = Action::Discard;
+ }
+ }
+ ctx.table_end();
+ }
+ if ctx.modal_end() {
+ action = Action::Cancel;
+ }
+
+ match action {
+ Action::None => return,
+ Action::Save => state.wants_save = true,
+ Action::Discard => state.documents.remove_active(),
+ Action::Cancel => state.wants_exit = false,
+ }
+
+ state.wants_close = false;
+ ctx.toss_focus_up();
+}
diff --git a/pkgs/edit/src/bin/edit/draw_filepicker.rs b/pkgs/edit/src/bin/edit/draw_filepicker.rs
new file mode 100644
index 0000000..79733d1
--- /dev/null
+++ b/pkgs/edit/src/bin/edit/draw_filepicker.rs
@@ -0,0 +1,258 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use std::cmp::Ordering;
+use std::fs;
+use std::path::PathBuf;
+
+use edit::framebuffer::IndexedColor;
+use edit::helpers::*;
+use edit::input::vk;
+use edit::tui::*;
+use edit::{icu, path, sys};
+
+use crate::localization::*;
+use crate::state::*;
+
+pub fn draw_file_picker(ctx: &mut Context, state: &mut State) {
+ // The save dialog is pre-filled with the current document filename.
+ if state.wants_file_picker == StateFilePicker::SaveAs {
+ state.wants_file_picker = StateFilePicker::SaveAsShown;
+
+ if state.file_picker_pending_name.as_os_str().is_empty() {
+ state.file_picker_pending_name =
+ state.documents.active().map_or("Untitled.txt", |doc| doc.filename.as_str()).into();
+ }
+ }
+
+ let width = (ctx.size().width - 20).max(10);
+ let height = (ctx.size().height - 10).max(10);
+ let mut doit = None;
+ let mut done = false;
+
+ ctx.modal_begin(
+ "file-picker",
+ if state.wants_file_picker == StateFilePicker::Open {
+ loc(LocId::FileOpen)
+ } else {
+ loc(LocId::FileSaveAs)
+ },
+ );
+ ctx.attr_intrinsic_size(Size { width, height });
+ {
+ let mut activated = false;
+
+ ctx.table_begin("path");
+ ctx.table_set_columns(&[0, COORD_TYPE_SAFE_MAX]);
+ ctx.table_set_cell_gap(Size { width: 1, height: 0 });
+ ctx.attr_padding(Rect::two(1, 1));
+ ctx.inherit_focus();
+ {
+ ctx.table_next_row();
+
+ ctx.label("dir-label", loc(LocId::SaveAsDialogPathLabel));
+ ctx.label("dir", state.file_picker_pending_dir.as_str());
+ ctx.attr_overflow(Overflow::TruncateMiddle);
+
+ ctx.table_next_row();
+ ctx.inherit_focus();
+
+ ctx.label("name-label", loc(LocId::SaveAsDialogNameLabel));
+ ctx.editline("name", &mut state.file_picker_pending_name);
+ ctx.inherit_focus();
+ if ctx.is_focused() && ctx.consume_shortcut(vk::RETURN) {
+ activated = true;
+ }
+ }
+ ctx.table_end();
+
+ if state.file_picker_entries.is_none() {
+ draw_dialog_saveas_refresh_files(state);
+ }
+
+ let files = state.file_picker_entries.as_ref().unwrap();
+
+ ctx.scrollarea_begin(
+ "directory",
+ Size {
+ width: 0,
+ // -1 for the label (top)
+ // -1 for the label (bottom)
+ // -1 for the editline (bottom)
+ height: height - 3,
+ },
+ );
+ ctx.attr_background_rgba(ctx.indexed_alpha(IndexedColor::Black, 1, 4));
+ ctx.next_block_id_mixin(state.file_picker_pending_dir.as_str().len() as u64);
+ {
+ ctx.list_begin("files");
+ ctx.inherit_focus();
+ for entry in files {
+ match ctx
+ .list_item(state.file_picker_pending_name == entry.as_path(), entry.as_str())
+ {
+ ListSelection::Unchanged => {}
+ ListSelection::Selected => {
+ state.file_picker_pending_name = entry.as_path().into()
+ }
+ ListSelection::Activated => activated = true,
+ }
+ ctx.attr_overflow(Overflow::TruncateMiddle);
+ }
+ ctx.list_end();
+
+ if ctx.contains_focus() && ctx.consume_shortcut(vk::BACK) {
+ state.file_picker_pending_name = "..".into();
+ activated = true;
+ }
+ }
+ ctx.scrollarea_end();
+
+ if activated {
+ doit = draw_file_picker_update_path(state);
+
+ // Check if the file already exists and show an overwrite warning in that case.
+ if state.wants_file_picker != StateFilePicker::Open
+ && let Some(path) = doit.as_deref()
+ && let Some(doc) = state.documents.active()
+ && let Some(file_id) = &doc.file_id
+ && sys::file_id_at(path).is_ok_and(|id| &id == file_id)
+ {
+ state.file_picker_overwrite_warning = doit.take();
+ }
+ }
+ }
+ if ctx.modal_end() {
+ done = true;
+ }
+
+ if state.file_picker_overwrite_warning.is_some() {
+ let mut save;
+
+ ctx.modal_begin("overwrite", loc(LocId::FileOverwriteWarning));
+ ctx.attr_background_rgba(ctx.indexed(IndexedColor::Red));
+ ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::BrightWhite));
+ {
+ ctx.label("description", loc(LocId::FileOverwriteWarningDescription));
+ ctx.attr_overflow(Overflow::TruncateTail);
+ ctx.attr_padding(Rect::three(1, 2, 1));
+
+ ctx.table_begin("choices");
+ ctx.inherit_focus();
+ ctx.attr_padding(Rect::three(0, 2, 1));
+ ctx.attr_position(Position::Center);
+ ctx.table_set_cell_gap(Size { width: 2, height: 0 });
+ {
+ ctx.table_next_row();
+ ctx.inherit_focus();
+
+ save = ctx.button("yes", loc(LocId::Yes));
+ ctx.inherit_focus();
+
+ if ctx.button("no", loc(LocId::No)) {
+ state.file_picker_overwrite_warning = None;
+ }
+ }
+ ctx.table_end();
+
+ save |= ctx.consume_shortcut(vk::Y);
+ if ctx.consume_shortcut(vk::N) {
+ state.file_picker_overwrite_warning = None;
+ }
+ }
+ if ctx.modal_end() {
+ state.file_picker_overwrite_warning = None;
+ }
+
+ if save {
+ doit = state.file_picker_overwrite_warning.take();
+ }
+ }
+
+ if let Some(path) = doit {
+ let res = if state.wants_file_picker == StateFilePicker::Open {
+ state.documents.add_file_path(&path).map(|_| ())
+ } else if let Some(doc) = state.documents.active_mut() {
+ doc.save(Some(path))
+ } else {
+ Ok(())
+ };
+ match res {
+ Ok(..) => {
+ ctx.needs_rerender();
+ done = true;
+ }
+ Err(err) => error_log_add(ctx, state, err),
+ }
+ }
+
+ if done {
+ state.wants_file_picker = StateFilePicker::None;
+ state.file_picker_pending_name = Default::default();
+ state.file_picker_entries = Default::default();
+ state.file_picker_overwrite_warning = Default::default();
+ }
+}
+
+// Returns Some(path) if the path refers to a file.
+fn draw_file_picker_update_path(state: &mut State) -> Option {
+ let path = state.file_picker_pending_dir.as_path();
+ let path = path.join(&state.file_picker_pending_name);
+ let path = path::normalize(&path);
+
+ let (dir, name) = if path.is_dir() {
+ (path.as_path(), PathBuf::new())
+ } else {
+ let dir = path.parent().unwrap_or(&path);
+ let name = path.file_name().map_or(Default::default(), |s| s.into());
+ (dir, name)
+ };
+ if dir != state.file_picker_pending_dir.as_path() {
+ state.file_picker_pending_dir = DisplayablePathBuf::new(dir.to_path_buf());
+ state.file_picker_entries = None;
+ }
+
+ state.file_picker_pending_name = name;
+ if state.file_picker_pending_name.as_os_str().is_empty() { None } else { Some(path) }
+}
+
+fn draw_dialog_saveas_refresh_files(state: &mut State) {
+ let dir = state.file_picker_pending_dir.as_path();
+ let mut files = Vec::new();
+
+ if dir.parent().is_some() {
+ files.push(DisplayablePathBuf::from(".."));
+ }
+
+ if let Ok(iter) = fs::read_dir(dir) {
+ for entry in iter.flatten() {
+ if let Ok(metadata) = entry.metadata() {
+ let mut name = entry.file_name();
+ if metadata.is_dir()
+ || (metadata.is_symlink()
+ && fs::metadata(entry.path()).is_ok_and(|m| m.is_dir()))
+ {
+ name.push("/");
+ }
+ files.push(DisplayablePathBuf::from(name));
+ }
+ }
+ }
+
+ // Sort directories first, then by name, case-insensitive.
+ let off = files.len().saturating_sub(1);
+ files[off..].sort_by(|a, b| {
+ let a = a.as_bytes();
+ let b = b.as_bytes();
+
+ let a_is_dir = a.last() == Some(&b'/');
+ let b_is_dir = b.last() == Some(&b'/');
+
+ match b_is_dir.cmp(&a_is_dir) {
+ Ordering::Equal => icu::compare_strings(a, b),
+ other => other,
+ }
+ });
+
+ state.file_picker_entries = Some(files);
+}
diff --git a/pkgs/edit/src/bin/edit/draw_menubar.rs b/pkgs/edit/src/bin/edit/draw_menubar.rs
new file mode 100644
index 0000000..95176c3
--- /dev/null
+++ b/pkgs/edit/src/bin/edit/draw_menubar.rs
@@ -0,0 +1,161 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use edit::arena_format;
+use edit::helpers::*;
+use edit::input::{kbmod, vk};
+use edit::tui::*;
+
+use crate::localization::*;
+use crate::state::*;
+
+pub fn draw_menubar(ctx: &mut Context, state: &mut State) {
+ ctx.menubar_begin();
+ ctx.attr_background_rgba(state.menubar_color_bg);
+ ctx.attr_foreground_rgba(state.menubar_color_fg);
+ {
+ if ctx.menubar_menu_begin(loc(LocId::File), 'F') {
+ draw_menu_file(ctx, state);
+ }
+ if state.documents.active().is_some() && ctx.menubar_menu_begin(loc(LocId::Edit), 'E') {
+ draw_menu_edit(ctx, state);
+ }
+ if ctx.menubar_menu_begin(loc(LocId::View), 'V') {
+ draw_menu_view(ctx, state);
+ }
+ if ctx.menubar_menu_begin(loc(LocId::Help), 'H') {
+ draw_menu_help(ctx, state);
+ }
+ }
+ ctx.menubar_end();
+}
+
+fn draw_menu_file(ctx: &mut Context, state: &mut State) {
+ if ctx.menubar_menu_button(loc(LocId::FileNew), 'N', kbmod::CTRL | vk::N) {
+ draw_add_untitled_document(ctx, state);
+ }
+ if ctx.menubar_menu_button(loc(LocId::FileOpen), 'O', kbmod::CTRL | vk::O) {
+ state.wants_file_picker = StateFilePicker::Open;
+ }
+ if state.documents.active().is_some() {
+ if ctx.menubar_menu_button(loc(LocId::FileSave), 'S', kbmod::CTRL | vk::S) {
+ state.wants_save = true;
+ }
+ if ctx.menubar_menu_button(loc(LocId::FileSaveAs), 'A', vk::NULL) {
+ state.wants_file_picker = StateFilePicker::SaveAs;
+ }
+ }
+ if ctx.menubar_menu_button(loc(LocId::FileClose), 'C', kbmod::CTRL | vk::W) {
+ state.wants_close = true;
+ }
+ if ctx.menubar_menu_button(loc(LocId::FileExit), 'X', kbmod::CTRL | vk::Q) {
+ state.wants_exit = true;
+ }
+ ctx.menubar_menu_end();
+}
+
+fn draw_menu_edit(ctx: &mut Context, state: &mut State) {
+ let doc = state.documents.active().unwrap();
+ let mut tb = doc.buffer.borrow_mut();
+
+ if ctx.menubar_menu_button(loc(LocId::EditUndo), 'U', kbmod::CTRL | vk::Z) {
+ tb.undo();
+ ctx.needs_rerender();
+ }
+ if ctx.menubar_menu_button(loc(LocId::EditRedo), 'R', kbmod::CTRL | vk::Y) {
+ tb.redo();
+ ctx.needs_rerender();
+ }
+ if ctx.menubar_menu_button(loc(LocId::EditCut), 'T', kbmod::CTRL | vk::X) {
+ ctx.set_clipboard(tb.extract_selection(true));
+ }
+ if ctx.menubar_menu_button(loc(LocId::EditCopy), 'C', kbmod::CTRL | vk::C) {
+ ctx.set_clipboard(tb.extract_selection(false));
+ }
+ if ctx.menubar_menu_button(loc(LocId::EditPaste), 'P', kbmod::CTRL | vk::V) {
+ tb.write(ctx.clipboard(), true);
+ ctx.needs_rerender();
+ }
+ if state.wants_search.kind != StateSearchKind::Disabled {
+ if ctx.menubar_menu_button(loc(LocId::EditFind), 'F', kbmod::CTRL | vk::F) {
+ state.wants_search.kind = StateSearchKind::Search;
+ state.wants_search.focus = true;
+ }
+ if ctx.menubar_menu_button(loc(LocId::EditReplace), 'R', kbmod::CTRL | vk::R) {
+ state.wants_search.kind = StateSearchKind::Replace;
+ state.wants_search.focus = true;
+ }
+ }
+ ctx.menubar_menu_end();
+}
+
+fn draw_menu_view(ctx: &mut Context, state: &mut State) {
+ if ctx.menubar_menu_button(loc(LocId::ViewFocusStatusbar), 'S', vk::NULL) {
+ state.wants_statusbar_focus = true;
+ }
+
+ if let Some(doc) = state.documents.active() {
+ let mut tb = doc.buffer.borrow_mut();
+ let word_wrap = tb.is_word_wrap_enabled();
+
+ if ctx.menubar_menu_checkbox(loc(LocId::ViewWordWrap), 'W', kbmod::ALT | vk::Z, word_wrap) {
+ tb.set_word_wrap(!word_wrap);
+ ctx.needs_rerender();
+ }
+ }
+
+ ctx.menubar_menu_end();
+}
+
+fn draw_menu_help(ctx: &mut Context, state: &mut State) {
+ if ctx.menubar_menu_button(loc(LocId::HelpAbout), 'A', vk::NULL) {
+ state.wants_about = true;
+ }
+ ctx.menubar_menu_end();
+}
+
+pub fn draw_dialog_about(ctx: &mut Context, state: &mut State) {
+ ctx.modal_begin("about", loc(LocId::AboutDialogTitle));
+ {
+ ctx.block_begin("content");
+ ctx.inherit_focus();
+ ctx.attr_padding(Rect::three(1, 2, 1));
+ {
+ ctx.label("description", "Microsoft Edit");
+ ctx.attr_overflow(Overflow::TruncateTail);
+ ctx.attr_position(Position::Center);
+
+ ctx.label(
+ "version",
+ &arena_format!(
+ ctx.arena(),
+ "{}{}",
+ loc(LocId::AboutDialogVersion),
+ env!("CARGO_PKG_VERSION")
+ ),
+ );
+ ctx.attr_overflow(Overflow::TruncateHead);
+ ctx.attr_position(Position::Center);
+
+ ctx.label("copyright", "Copyright (c) Microsoft Corp 2025");
+ ctx.attr_overflow(Overflow::TruncateTail);
+ ctx.attr_position(Position::Center);
+
+ ctx.block_begin("choices");
+ ctx.inherit_focus();
+ ctx.attr_padding(Rect::three(1, 2, 0));
+ ctx.attr_position(Position::Center);
+ {
+ if ctx.button("ok", loc(LocId::Ok)) {
+ state.wants_about = false;
+ }
+ ctx.inherit_focus();
+ }
+ ctx.block_end();
+ }
+ ctx.block_end();
+ }
+ if ctx.modal_end() {
+ state.wants_about = false;
+ }
+}
diff --git a/pkgs/edit/src/bin/edit/draw_statusbar.rs b/pkgs/edit/src/bin/edit/draw_statusbar.rs
new file mode 100644
index 0000000..ecbc5e4
--- /dev/null
+++ b/pkgs/edit/src/bin/edit/draw_statusbar.rs
@@ -0,0 +1,291 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use edit::framebuffer::{Attributes, IndexedColor};
+use edit::helpers::*;
+use edit::input::vk;
+use edit::tui::*;
+use edit::{arena_format, icu};
+
+use crate::localization::*;
+use crate::state::*;
+
+pub fn draw_statusbar(ctx: &mut Context, state: &mut State) {
+ ctx.table_begin("statusbar");
+ ctx.attr_focus_well();
+ ctx.attr_background_rgba(state.menubar_color_bg);
+ ctx.attr_foreground_rgba(state.menubar_color_fg);
+ ctx.table_set_cell_gap(Size { width: 2, height: 0 });
+ ctx.attr_intrinsic_size(Size { width: COORD_TYPE_SAFE_MAX, height: 1 });
+ ctx.attr_padding(Rect::two(0, 1));
+
+ if let Some(doc) = state.documents.active() {
+ let mut tb = doc.buffer.borrow_mut();
+
+ ctx.table_next_row();
+
+ if ctx.button("newline", if tb.is_crlf() { "CRLF" } else { "LF" }) {
+ let is_crlf = tb.is_crlf();
+ tb.normalize_newlines(!is_crlf);
+ }
+ if state.wants_statusbar_focus {
+ state.wants_statusbar_focus = false;
+ ctx.steal_focus();
+ }
+
+ state.wants_encoding_picker |= ctx.button("encoding", tb.encoding());
+ if state.wants_encoding_picker {
+ if doc.path.is_some() {
+ ctx.block_begin("frame");
+ ctx.attr_float(FloatSpec {
+ anchor: Anchor::Last,
+ gravity_x: 0.0,
+ gravity_y: 1.0,
+ offset_x: 0.0,
+ offset_y: 0.0,
+ });
+ ctx.attr_padding(Rect::two(0, 1));
+ ctx.attr_border();
+ {
+ if ctx.button("reopen", loc(LocId::EncodingReopen)) {
+ state.wants_encoding_change = StateEncodingChange::Reopen;
+ }
+ ctx.focus_on_first_present();
+ if ctx.button("convert", loc(LocId::EncodingConvert)) {
+ state.wants_encoding_change = StateEncodingChange::Convert;
+ }
+ }
+ ctx.block_end();
+ } else {
+ // Can't reopen a file that doesn't exist.
+ state.wants_encoding_change = StateEncodingChange::Convert;
+ }
+
+ if !ctx.contains_focus() {
+ state.wants_encoding_picker = false;
+ ctx.needs_rerender();
+ }
+ }
+
+ state.wants_indentation_picker |= ctx.button(
+ "indentation",
+ &arena_format!(
+ ctx.arena(),
+ "{}:{}",
+ loc(if tb.indent_with_tabs() {
+ LocId::IndentationTabs
+ } else {
+ LocId::IndentationSpaces
+ }),
+ tb.tab_size(),
+ ),
+ );
+ if state.wants_indentation_picker {
+ ctx.table_begin("indentation-picker");
+ ctx.attr_float(FloatSpec {
+ anchor: Anchor::Last,
+ gravity_x: 0.0,
+ gravity_y: 1.0,
+ offset_x: 0.0,
+ offset_y: 0.0,
+ });
+ ctx.attr_border();
+ ctx.attr_padding(Rect::two(0, 1));
+ ctx.table_set_cell_gap(Size { width: 1, height: 0 });
+ {
+ if ctx.consume_shortcut(vk::RETURN) {
+ ctx.toss_focus_up();
+ }
+
+ ctx.table_next_row();
+
+ ctx.list_begin("type");
+ ctx.focus_on_first_present();
+ ctx.attr_padding(Rect::two(0, 1));
+ {
+ if ctx.list_item(tb.indent_with_tabs(), loc(LocId::IndentationTabs))
+ != ListSelection::Unchanged
+ {
+ tb.set_indent_with_tabs(true);
+ ctx.needs_rerender();
+ }
+ if ctx.list_item(!tb.indent_with_tabs(), loc(LocId::IndentationSpaces))
+ != ListSelection::Unchanged
+ {
+ tb.set_indent_with_tabs(false);
+ ctx.needs_rerender();
+ }
+ }
+ ctx.list_end();
+
+ ctx.list_begin("width");
+ ctx.attr_padding(Rect::two(0, 2));
+ {
+ for width in 1u8..=8 {
+ let ch = [b'0' + width];
+ let label = unsafe { std::str::from_utf8_unchecked(&ch) };
+
+ if ctx.list_item(tb.tab_size() == width as CoordType, label)
+ != ListSelection::Unchanged
+ {
+ tb.set_tab_size(width as CoordType);
+ ctx.needs_rerender();
+ }
+ }
+ }
+ ctx.list_end();
+ }
+ ctx.table_end();
+
+ if !ctx.contains_focus() {
+ state.wants_indentation_picker = false;
+ ctx.needs_rerender();
+ }
+ }
+
+ ctx.label(
+ "location",
+ &arena_format!(
+ ctx.arena(),
+ "{}:{}",
+ tb.cursor_logical_pos().y + 1,
+ tb.cursor_logical_pos().x + 1
+ ),
+ );
+
+ #[cfg(any(feature = "debug-layout", feature = "debug-latency"))]
+ ctx.label(
+ "stats",
+ &arena_format!(ctx.arena(), "{}/{}", tb.logical_line_count(), tb.visual_line_count(),),
+ );
+
+ if tb.is_overtype() && ctx.button("overtype", "OVR") {
+ tb.set_overtype(false);
+ ctx.needs_rerender();
+ }
+
+ if tb.is_dirty() {
+ ctx.label("dirty", "*");
+ }
+
+ ctx.block_begin("filename-container");
+ ctx.attr_intrinsic_size(Size { width: COORD_TYPE_SAFE_MAX, height: 1 });
+ {
+ let total = state.documents.len();
+ let mut filename = doc.filename.as_str();
+ let filename_buf;
+
+ if total > 1 {
+ filename_buf = arena_format!(ctx.arena(), "{} + {}", filename, total - 1);
+ filename = &filename_buf;
+ }
+
+ state.wants_document_picker |= ctx.button("filename", filename);
+ ctx.inherit_focus();
+ ctx.attr_overflow(Overflow::TruncateMiddle);
+ ctx.attr_position(Position::Right);
+ }
+ ctx.block_end();
+ }
+
+ ctx.table_end();
+}
+
+pub fn draw_dialog_encoding_change(ctx: &mut Context, state: &mut State) {
+ let doc = state.documents.active_mut().unwrap();
+ let reopen = state.wants_encoding_change == StateEncodingChange::Reopen;
+ let width = (ctx.size().width - 20).max(10);
+ let height = (ctx.size().height - 10).max(10);
+ let mut change = None;
+
+ ctx.modal_begin(
+ "encode",
+ if reopen { loc(LocId::EncodingReopen) } else { loc(LocId::EncodingConvert) },
+ );
+ {
+ ctx.scrollarea_begin("scrollarea", Size { width, height });
+ ctx.attr_background_rgba(ctx.indexed_alpha(IndexedColor::Black, 1, 4));
+ ctx.inherit_focus();
+ {
+ let encodings = icu::get_available_encodings();
+
+ ctx.list_begin("encodings");
+ ctx.inherit_focus();
+ for &encoding in encodings {
+ if ctx.list_item(encoding == doc.buffer.borrow().encoding(), encoding)
+ == ListSelection::Activated
+ {
+ change = Some(encoding);
+ break;
+ }
+ }
+ ctx.list_end();
+ }
+ ctx.scrollarea_end();
+ }
+ if ctx.modal_end() {
+ state.wants_encoding_change = StateEncodingChange::None;
+ }
+
+ if let Some(encoding) = change {
+ if reopen && doc.path.is_some() {
+ let mut res = Ok(());
+ if doc.buffer.borrow().is_dirty() {
+ res = doc.save(None);
+ }
+ if res.is_ok() {
+ res = doc.reread(Some(encoding));
+ }
+ if let Err(err) = res {
+ error_log_add(ctx, state, err);
+ }
+ } else {
+ doc.buffer.borrow_mut().set_encoding(encoding);
+ }
+
+ state.wants_encoding_change = StateEncodingChange::None;
+ ctx.needs_rerender();
+ }
+}
+
+pub fn draw_document_picker(ctx: &mut Context, state: &mut State) {
+ ctx.modal_begin("document-picker", "");
+ {
+ let width = (ctx.size().width - 20).max(10);
+ let height = (ctx.size().height - 10).max(10);
+
+ ctx.scrollarea_begin("scrollarea", Size { width, height });
+ ctx.attr_background_rgba(ctx.indexed_alpha(IndexedColor::Black, 1, 4));
+ ctx.inherit_focus();
+ {
+ ctx.list_begin("documents");
+ ctx.inherit_focus();
+
+ if state.documents.update_active(|doc| {
+ let tb = doc.buffer.borrow();
+
+ ctx.styled_list_item_begin();
+ ctx.attr_overflow(Overflow::TruncateTail);
+ ctx.styled_label_add_text(if tb.is_dirty() { "* " } else { " " });
+ ctx.styled_label_add_text(&doc.filename);
+
+ if let Some(path) = &doc.dir {
+ ctx.styled_label_add_text(" ");
+ ctx.styled_label_set_attributes(Attributes::Italic);
+ ctx.styled_label_add_text(path.as_str());
+ }
+
+ ctx.styled_list_item_end(false) == ListSelection::Activated
+ }) {
+ state.wants_document_picker = false;
+ ctx.needs_rerender();
+ }
+
+ ctx.list_end();
+ }
+ ctx.scrollarea_end();
+ }
+ if ctx.modal_end() {
+ state.wants_document_picker = false;
+ }
+}
diff --git a/pkgs/edit/src/bin/edit/edit.exe.manifest b/pkgs/edit/src/bin/edit/edit.exe.manifest
new file mode 100644
index 0000000..b972407
--- /dev/null
+++ b/pkgs/edit/src/bin/edit/edit.exe.manifest
@@ -0,0 +1,22 @@
+
+
+
+
+ true
+ UTF-8
+ SegmentHeap
+
+
+
+
+
+
+
+
diff --git a/pkgs/edit/src/bin/edit/localization.rs b/pkgs/edit/src/bin/edit/localization.rs
new file mode 100644
index 0000000..115f888
--- /dev/null
+++ b/pkgs/edit/src/bin/edit/localization.rs
@@ -0,0 +1,950 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use edit::arena::scratch_arena;
+use edit::sys;
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum LocId {
+ Ctrl,
+ Alt,
+ Shift,
+
+ Ok,
+ Yes,
+ No,
+ Cancel,
+ Always,
+
+ // File menu
+ File,
+ FileNew,
+ FileOpen,
+ FileSave,
+ FileSaveAs,
+ FileClose,
+ FileExit,
+
+ // Edit menu
+ Edit,
+ EditUndo,
+ EditRedo,
+ EditCut,
+ EditCopy,
+ EditPaste,
+ EditFind,
+ EditReplace,
+
+ // View menu
+ View,
+ ViewFocusStatusbar,
+ ViewWordWrap,
+
+ // Help menu
+ Help,
+ HelpAbout,
+
+ // Exit dialog
+ UnsavedChangesDialogTitle,
+ UnsavedChangesDialogDescription,
+ UnsavedChangesDialogYes,
+ UnsavedChangesDialogNo,
+
+ // About dialog
+ AboutDialogTitle,
+ AboutDialogVersion,
+
+ // Shown when the clipboard size exceeds the limit for OSC 52
+ LargeClipboardWarningLine1,
+ LargeClipboardWarningLine2,
+ LargeClipboardWarningLine3,
+ SuperLargeClipboardWarning,
+
+ // Warning dialog
+ WarningDialogTitle,
+
+ // Error dialog
+ ErrorDialogTitle,
+ ErrorIcuMissing,
+
+ SearchNeedleLabel,
+ SearchReplacementLabel,
+ SearchMatchCase,
+ SearchWholeWord,
+ SearchUseRegex,
+ SearchReplaceAll,
+ SearchClose,
+
+ EncodingReopen,
+ EncodingConvert,
+
+ IndentationTabs,
+ IndentationSpaces,
+
+ SaveAsDialogPathLabel,
+ SaveAsDialogNameLabel,
+
+ FileOverwriteWarning,
+ FileOverwriteWarningDescription,
+
+ Count,
+}
+
+#[allow(non_camel_case_types)]
+#[derive(Clone, Copy, PartialEq, Eq)]
+enum LangId {
+ // Base language. It's always the first one.
+ en,
+
+ // Other languages. Sorted alphabetically.
+ de,
+ es,
+ fr,
+ it,
+ ja,
+ ko,
+ pt_br,
+ ru,
+ zh_hans,
+ zh_hant,
+
+ Count,
+}
+
+#[rustfmt::skip]
+const S_LANG_LUT: [[&str; LangId::Count as usize]; LocId::Count as usize] = [
+ // Ctrl (the keyboard key)
+ [
+ /* en */ "Ctrl",
+ /* de */ "Strg",
+ /* es */ "Ctrl",
+ /* fr */ "Ctrl",
+ /* it */ "Ctrl",
+ /* ja */ "Ctrl",
+ /* ko */ "Ctrl",
+ /* pt_br */ "Ctrl",
+ /* ru */ "Ctrl",
+ /* zh_hans */ "Ctrl",
+ /* zh_hant */ "Ctrl",
+ ],
+ // Alt (the keyboard key)
+ [
+ /* en */ "Alt",
+ /* de */ "Alt",
+ /* es */ "Alt",
+ /* fr */ "Alt",
+ /* it */ "Alt",
+ /* ja */ "Alt",
+ /* ko */ "Alt",
+ /* pt_br */ "Alt",
+ /* ru */ "Alt",
+ /* zh_hans */ "Alt",
+ /* zh_hant */ "Alt",
+ ],
+ // Shift (the keyboard key)
+ [
+ /* en */ "Shift",
+ /* de */ "Umschalt",
+ /* es */ "Mayús",
+ /* fr */ "Maj",
+ /* it */ "Maiusc",
+ /* ja */ "Shift",
+ /* ko */ "Shift",
+ /* pt_br */ "Shift",
+ /* ru */ "Shift",
+ /* zh_hans */ "Shift",
+ /* zh_hant */ "Shift",
+ ],
+
+ // Ok (used as a common dialog button)
+ [
+ /* en */ "Ok",
+ /* de */ "OK",
+ /* es */ "Aceptar",
+ /* fr */ "OK",
+ /* it */ "OK",
+ /* ja */ "OK",
+ /* ko */ "확인",
+ /* pt_br */ "OK",
+ /* ru */ "ОК",
+ /* zh_hans */ "确定",
+ /* zh_hant */ "確定",
+ ],
+ // Yes (used as a common dialog button)
+ [
+ /* en */ "Yes",
+ /* de */ "Ja",
+ /* es */ "Sí",
+ /* fr */ "Oui",
+ /* it */ "Sì",
+ /* ja */ "はい",
+ /* ko */ "예",
+ /* pt_br */ "Sim",
+ /* ru */ "Да",
+ /* zh_hans */ "是",
+ /* zh_hant */ "是",
+ ],
+ // No (used as a common dialog button)
+ [
+ /* en */ "No",
+ /* de */ "Nein",
+ /* es */ "No",
+ /* fr */ "Non",
+ /* it */ "No",
+ /* ja */ "いいえ",
+ /* ko */ "아니오",
+ /* pt_br */ "Não",
+ /* ru */ "Нет",
+ /* zh_hans */ "否",
+ /* zh_hant */ "否",
+ ],
+ // Cancel (used as a common dialog button)
+ [
+ /* en */ "Cancel",
+ /* de */ "Abbrechen",
+ /* es */ "Cancelar",
+ /* fr */ "Annuler",
+ /* it */ "Annulla",
+ /* ja */ "キャンセル",
+ /* ko */ "취소",
+ /* pt_br */ "Cancelar",
+ /* ru */ "Отмена",
+ /* zh_hans */ "取消",
+ /* zh_hant */ "取消",
+ ],
+ // Always (used as a common dialog button)
+ [
+ /* en */ "Always",
+ /* de */ "Immer",
+ /* es */ "Siempre",
+ /* fr */ "Toujours",
+ /* it */ "Sempre",
+ /* ja */ "常に",
+ /* ko */ "항상",
+ /* pt_br */ "Sempre",
+ /* ru */ "Всегда",
+ /* zh_hans */ "总是",
+ /* zh_hant */ "總是",
+ ],
+
+ // File (a menu bar item)
+ [
+ /* en */ "File",
+ /* de */ "Datei",
+ /* es */ "Archivo",
+ /* fr */ "Fichier",
+ /* it */ "File",
+ /* ja */ "ファイル",
+ /* ko */ "파일",
+ /* pt_br */ "Arquivo",
+ /* ru */ "Файл",
+ /* zh_hans */ "文件",
+ /* zh_hant */ "檔案",
+ ],
+ // FileNew
+ [
+ /* en */ "New File…",
+ /* de */ "Neue Datei…",
+ /* es */ "Nuevo archivo…",
+ /* fr */ "Nouveau fichier…",
+ /* it */ "Nuovo file…",
+ /* ja */ "新規ファイル…",
+ /* ko */ "새 파일…",
+ /* pt_br */ "Novo arquivo…",
+ /* ru */ "Новый файл…",
+ /* zh_hans */ "新建文件…",
+ /* zh_hant */ "新增檔案…",
+ ],
+ // FileOpen
+ [
+ /* en */ "Open File…",
+ /* de */ "Datei öffnen…",
+ /* es */ "Abrir archivo…",
+ /* fr */ "Ouvrir un fichier…",
+ /* it */ "Apri file…",
+ /* ja */ "ファイルを開く…",
+ /* ko */ "파일 열기…",
+ /* pt_br */ "Abrir arquivo…",
+ /* ru */ "Открыть файл…",
+ /* zh_hans */ "打开文件…",
+ /* zh_hant */ "開啟檔案…",
+ ],
+ // FileSave
+ [
+ /* en */ "Save",
+ /* de */ "Speichern",
+ /* es */ "Guardar",
+ /* fr */ "Enregistrer",
+ /* it */ "Salva",
+ /* ja */ "保存",
+ /* ko */ "저장",
+ /* pt_br */ "Salvar",
+ /* ru */ "Сохранить",
+ /* zh_hans */ "保存",
+ /* zh_hant */ "儲存",
+ ],
+ // FileSaveAs
+ [
+ /* en */ "Save As…",
+ /* de */ "Speichern unter…",
+ /* es */ "Guardar como…",
+ /* fr */ "Enregistrer sous…",
+ /* it */ "Salva come…",
+ /* ja */ "名前を付けて保存…",
+ /* ko */ "다른 이름으로 저장…",
+ /* pt_br */ "Salvar como…",
+ /* ru */ "Сохранить как…",
+ /* zh_hans */ "另存为…",
+ /* zh_hant */ "另存新檔…",
+ ],
+ // FileClose
+ [
+ /* en */ "Close Editor",
+ /* de */ "Editor schließen",
+ /* es */ "Cerrar editor",
+ /* fr */ "Fermer l'éditeur",
+ /* it */ "Chiudi editor",
+ /* ja */ "エディターを閉じる",
+ /* ko */ "편집기 닫기",
+ /* pt_br */ "Fechar editor",
+ /* ru */ "Закрыть редактор",
+ /* zh_hans */ "关闭编辑器",
+ /* zh_hant */ "關閉編輯器",
+ ],
+ // FileExit
+ [
+ /* en */ "Exit",
+ /* de */ "Beenden",
+ /* es */ "Salir",
+ /* fr */ "Quitter",
+ /* it */ "Esci",
+ /* ja */ "終了",
+ /* ko */ "종료",
+ /* pt_br */ "Sair",
+ /* ru */ "Выход",
+ /* zh_hans */ "退出",
+ /* zh_hant */ "退出",
+ ],
+
+ // Edit (a menu bar item)
+ [
+ /* en */ "Edit",
+ /* de */ "Bearbeiten",
+ /* es */ "Editar",
+ /* fr */ "Éditer",
+ /* it */ "Modifica",
+ /* ja */ "編集",
+ /* ko */ "편집",
+ /* pt_br */ "Editar",
+ /* ru */ "Правка",
+ /* zh_hans */ "编辑",
+ /* zh_hant */ "編輯",
+ ],
+ // EditUndo
+ [
+ /* en */ "Undo",
+ /* de */ "Rückgängig",
+ /* es */ "Deshacer",
+ /* fr */ "Annuler",
+ /* it */ "Annulla",
+ /* ja */ "元に戻す",
+ /* ko */ "실행 취소",
+ /* pt_br */ "Desfazer",
+ /* ru */ "Отменить",
+ /* zh_hans */ "撤销",
+ /* zh_hant */ "復原",
+ ],
+ // EditRedo
+ [
+ /* en */ "Redo",
+ /* de */ "Wiederholen",
+ /* es */ "Rehacer",
+ /* fr */ "Rétablir",
+ /* it */ "Ripeti",
+ /* ja */ "やり直し",
+ /* ko */ "다시 실행",
+ /* pt_br */ "Refazer",
+ /* ru */ "Повторить",
+ /* zh_hans */ "重做",
+ /* zh_hant */ "重做",
+ ],
+ // EditCut
+ [
+ /* en */ "Cut",
+ /* de */ "Ausschneiden",
+ /* es */ "Cortar",
+ /* fr */ "Couper",
+ /* it */ "Taglia",
+ /* ja */ "切り取り",
+ /* ko */ "잘라내기",
+ /* pt_br */ "Cortar",
+ /* ru */ "Вырезать",
+ /* zh_hans */ "剪切",
+ /* zh_hant */ "剪下",
+ ],
+ // EditCopy
+ [
+ /* en */ "Copy",
+ /* de */ "Kopieren",
+ /* es */ "Copiar",
+ /* fr */ "Copier",
+ /* it */ "Copia",
+ /* ja */ "コピー",
+ /* ko */ "복사",
+ /* pt_br */ "Copiar",
+ /* ru */ "Копировать",
+ /* zh_hans */ "复制",
+ /* zh_hant */ "複製",
+ ],
+ // EditPaste
+ [
+ /* en */ "Paste",
+ /* de */ "Einfügen",
+ /* es */ "Pegar",
+ /* fr */ "Coller",
+ /* it */ "Incolla",
+ /* ja */ "貼り付け",
+ /* ko */ "붙여넣기",
+ /* pt_br */ "Colar",
+ /* ru */ "Вставить",
+ /* zh_hans */ "粘贴",
+ /* zh_hant */ "貼上",
+ ],
+ // EditFind
+ [
+ /* en */ "Find",
+ /* de */ "Suchen",
+ /* es */ "Buscar",
+ /* fr */ "Rechercher",
+ /* it */ "Trova",
+ /* ja */ "検索",
+ /* ko */ "찾기",
+ /* pt_br */ "Encontrar",
+ /* ru */ "Найти",
+ /* zh_hans */ "查找",
+ /* zh_hant */ "尋找",
+ ],
+ // EditReplace
+ [
+ /* en */ "Replace",
+ /* de */ "Ersetzen",
+ /* es */ "Reemplazar",
+ /* fr */ "Remplacer",
+ /* it */ "Sostituisci",
+ /* ja */ "置換",
+ /* ko */ "바꾸기",
+ /* pt_br */ "Substituir",
+ /* ru */ "Заменить",
+ /* zh_hans */ "替换",
+ /* zh_hant */ "取代",
+ ],
+
+ // View (a menu bar item)
+ [
+ /* en */ "View",
+ /* de */ "Ansicht",
+ /* es */ "Ver",
+ /* fr */ "Affichage",
+ /* it */ "Visualizza",
+ /* ja */ "表示",
+ /* ko */ "보기",
+ /* pt_br */ "Exibir",
+ /* ru */ "Вид",
+ /* zh_hans */ "视图",
+ /* zh_hant */ "檢視",
+ ],
+ // ViewFocusStatusbar
+ [
+ /* en */ "Focus Statusbar",
+ /* de */ "Statusleiste fokussieren",
+ /* es */ "Enfocar barra de estado",
+ /* fr */ "Focus sur la barre d'état",
+ /* it */ "Attiva barra di stato",
+ /* ja */ "ステータスバーにフォーカス",
+ /* ko */ "상태 표시줄로 포커스 이동",
+ /* pt_br */ "Focar barra de status",
+ /* ru */ "Фокус на строку состояния",
+ /* zh_hans */ "聚焦状态栏",
+ /* zh_hant */ "聚焦狀態列",
+ ],
+ // ViewWordWrap
+ [
+ /* en */ "Word Wrap",
+ /* de */ "Zeilenumbruch",
+ /* es */ "Ajuste de línea",
+ /* fr */ "Retour à la ligne",
+ /* it */ "A capo automatico",
+ /* ja */ "折り返し",
+ /* ko */ "자동 줄 바꿈",
+ /* pt_br */ "Quebra de linha",
+ /* ru */ "Перенос слов",
+ /* zh_hans */ "自动换行",
+ /* zh_hant */ "自動換行",
+ ],
+
+ // Help (a menu bar item)
+ [
+ /* en */ "Help",
+ /* de */ "Hilfe",
+ /* es */ "Ayuda",
+ /* fr */ "Aide",
+ /* it */ "Aiuto",
+ /* ja */ "ヘルプ",
+ /* ko */ "도움말",
+ /* pt_br */ "Ajuda",
+ /* ru */ "Помощь",
+ /* zh_hans */ "帮助",
+ /* zh_hant */ "幫助",
+ ],
+ // HelpAbout
+ [
+ /* en */ "About",
+ /* de */ "Über",
+ /* es */ "Acerca de",
+ /* fr */ "À propos",
+ /* it */ "Informazioni",
+ /* ja */ "情報",
+ /* ko */ "정보",
+ /* pt_br */ "Sobre",
+ /* ru */ "О программе",
+ /* zh_hans */ "关于",
+ /* zh_hant */ "關於",
+ ],
+
+ // UnsavedChangesDialogTitle
+ [
+ /* en */ "Unsaved Changes",
+ /* de */ "Ungespeicherte Änderungen",
+ /* es */ "Cambios sin guardar",
+ /* fr */ "Modifications non enregistrées",
+ /* it */ "Modifiche non salvate",
+ /* ja */ "未保存の変更",
+ /* ko */ "저장되지 않은 변경 사항",
+ /* pt_br */ "Alterações não salvas",
+ /* ru */ "Несохраненные изменения",
+ /* zh_hans */ "未保存的更改",
+ /* zh_hant */ "未儲存的變更",
+ ],
+ // UnsavedChangesDialogDescription
+ [
+ /* en */ "Do you want to save the changes you made?",
+ /* de */ "Möchten Sie die vorgenommenen Änderungen speichern?",
+ /* es */ "¿Desea guardar los cambios realizados?",
+ /* fr */ "Voulez-vous enregistrer les modifications apportées?",
+ /* it */ "Vuoi salvare le modifiche apportate?",
+ /* ja */ "変更内容を保存しますか?",
+ /* ko */ "변경한 내용을 저장하시겠습니까?",
+ /* pt_br */ "Deseja salvar as alterações feitas?",
+ /* ru */ "Вы хотите сохранить внесённые изменения?",
+ /* zh_hans */ "您要保存所做的更改吗?",
+ /* zh_hant */ "您要保存所做的變更嗎?",
+ ],
+ // UnsavedChangesDialogYes
+ [
+ /* en */ "Save",
+ /* de */ "Speichern",
+ /* es */ "Guardar",
+ /* fr */ "Enregistrer",
+ /* it */ "Salva",
+ /* ja */ "保存",
+ /* ko */ "저장",
+ /* pt_br */ "Salvar",
+ /* ru */ "Сохранить",
+ /* zh_hans */ "保存",
+ /* zh_hant */ "儲存",
+ ],
+ // UnsavedChangesDialogNo
+ [
+ /* en */ "Don't Save",
+ /* de */ "Nicht speichern",
+ /* es */ "No guardar",
+ /* fr */ "Ne pas enregistrer",
+ /* it */ "Non salvare",
+ /* ja */ "保存しない",
+ /* ko */ "저장 안 함",
+ /* pt_br */ "Não salvar",
+ /* ru */ "Не сохранять",
+ /* zh_hans */ "不保存",
+ /* zh_hant */ "不儲存",
+ ],
+
+ // AboutDialogTitle
+ [
+ /* en */ "About",
+ /* de */ "Über",
+ /* es */ "Acerca de",
+ /* fr */ "À propos",
+ /* it */ "Informazioni",
+ /* ja */ "情報",
+ /* ko */ "정보",
+ /* pt_br */ "Sobre",
+ /* ru */ "О программе",
+ /* zh_hans */ "关于",
+ /* zh_hant */ "關於",
+ ],
+ // AboutDialogVersion
+ [
+ /* en */ "Version: ",
+ /* de */ "Version: ",
+ /* es */ "Versión: ",
+ /* fr */ "Version: ",
+ /* it */ "Versione: ",
+ /* ja */ "バージョン: ",
+ /* ko */ "버전: ",
+ /* pt_br */ "Versão: ",
+ /* ru */ "Версия: ",
+ /* zh_hans */ "版本: ",
+ /* zh_hant */ "版本: ",
+ ],
+
+ // Shown when the clipboard size exceeds the limit for OSC 52
+ // LargeClipboardWarningLine1
+ [
+ /* en */ "Text you copy is shared with the terminal clipboard.",
+ /* de */ "Der kopierte Text wird mit der Terminal-Zwischenablage geteilt.",
+ /* es */ "El texto que copies se comparte con el portapapeles del terminal.",
+ /* fr */ "Le texte que vous copiez est partagé avec le presse-papiers du terminal.",
+ /* it */ "Il testo copiato viene condiviso con gli appunti del terminale.",
+ /* ja */ "コピーしたテキストはターミナルのクリップボードと共有されます。",
+ /* ko */ "복사한 텍스트가 터미널 클립보드와 공유됩니다.",
+ /* pt_br */ "O texto copiado é compartilhado com a área de transferência do terminal.",
+ /* ru */ "Скопированный текст передаётся в буфер обмена терминала.",
+ /* zh_hans */ "你复制的文本将共享到终端剪贴板。",
+ /* zh_hant */ "您複製的文字將會與終端機剪貼簿分享。",
+ ],
+ // LargeClipboardWarningLine2
+ [
+ /* en */ "You copied {size} which may take a long time to share.",
+ /* de */ "Sie haben {size} kopiert, das Weitergeben könnte lange dauern.",
+ /* es */ "Copiaste {size}, lo que puede tardar en compartirse.",
+ /* fr */ "Vous avez copié {size}, ce qui peut être long à partager.",
+ /* it */ "Hai copiato {size}, potrebbe richiedere molto tempo per condividerlo.",
+ /* ja */ "{size} をコピーしました。共有に時間がかかる可能性があります。",
+ /* ko */ "{size}를 복사했습니다. 공유하는 데 시간이 오래 걸릴 수 있습니다.",
+ /* pt_br */ "Você copiou {size}, o que pode demorar para compartilhar.",
+ /* ru */ "Вы скопировали {size}; передача может занять много времени.",
+ /* zh_hans */ "你复制了 {size},共享可能需要较长时间。",
+ /* zh_hant */ "您已複製 {size},共享可能需要較長時間。",
+ ],
+ // LargeClipboardWarningLine3
+ [
+ /* en */ "Do you want to send it anyway?",
+ /* de */ "Möchten Sie es trotzdem senden?",
+ /* es */ "¿Desea enviarlo de todas formas?",
+ /* fr */ "Voulez-vous quand même l’envoyer?",
+ /* it */ "Vuoi inviarlo comunque?",
+ /* ja */ "それでも送信しますか?",
+ /* ko */ "그래도 전송하시겠습니까?",
+ /* pt_br */ "Deseja enviar mesmo assim?",
+ /* ru */ "Отправить в любом случае?",
+ /* zh_hans */ "仍要发送吗?",
+ /* zh_hant */ "仍要傳送嗎?",
+ ],
+ // SuperLargeClipboardWarning (as an alternative to LargeClipboardWarningLine2 and 3)
+ [
+ /* en */ "The text you copied is too large to be shared.",
+ /* de */ "Der kopierte Text ist zu groß, um geteilt zu werden.",
+ /* es */ "El texto que copiaste es demasiado grande para compartirse.",
+ /* fr */ "Le texte que vous avez copié est trop volumineux pour être partagé.",
+ /* it */ "Il testo copiato è troppo grande per essere condiviso.",
+ /* ja */ "コピーしたテキストは大きすぎて共有できません。",
+ /* ko */ "복사한 텍스트가 너무 커서 공유할 수 없습니다.",
+ /* pt_br */ "O texto copiado é grande demais para ser compartilhado.",
+ /* ru */ "Скопированный текст слишком велик для передачи.",
+ /* zh_hans */ "你复制的文本过大,无法共享。",
+ /* zh_hant */ "您複製的文字過大,無法分享。",
+ ],
+
+ // WarningDialogTitle
+ [
+ /* en */ "Warning",
+ /* de */ "Warnung",
+ /* es */ "Advertencia",
+ /* fr */ "Avertissement",
+ /* it */ "Avviso",
+ /* ja */ "警告",
+ /* ko */ "경고",
+ /* pt_br */ "Aviso",
+ /* ru */ "Предупреждение",
+ /* zh_hans */ "警告",
+ /* zh_hant */ "警告",
+ ],
+
+ // ErrorDialogTitle
+ [
+ /* en */ "Error",
+ /* de */ "Fehler",
+ /* es */ "Error",
+ /* fr */ "Erreur",
+ /* it */ "Errore",
+ /* ja */ "エラー",
+ /* ko */ "오류",
+ /* pt_br */ "Erro",
+ /* ru */ "Ошибка",
+ /* zh_hans */ "错误",
+ /* zh_hant */ "錯誤",
+ ],
+ // ErrorIcuMissing
+ [
+ /* en */ "This operation requires the ICU library",
+ /* de */ "Diese Operation erfordert die ICU-Bibliothek",
+ /* es */ "Esta operación requiere la biblioteca ICU",
+ /* fr */ "Cette opération nécessite la bibliothèque ICU",
+ /* it */ "Questa operazione richiede la libreria ICU",
+ /* ja */ "この操作にはICUライブラリが必要です",
+ /* ko */ "이 작업에는 ICU 라이브러리가 필요합니다",
+ /* pt_br */ "Esta operação requer a biblioteca ICU",
+ /* ru */ "Эта операция требует наличия библиотеки ICU",
+ /* zh_hans */ "此操作需要 ICU 库",
+ /* zh_hant */ "此操作需要 ICU 庫",
+ ],
+
+ // SearchNeedleLabel (for input field)
+ [
+ /* en */ "Find:",
+ /* de */ "Suchen:",
+ /* es */ "Buscar:",
+ /* fr */ "Rechercher:",
+ /* it */ "Trova:",
+ /* ja */ "検索:",
+ /* ko */ "찾기:",
+ /* pt_br */ "Encontrar:",
+ /* ru */ "Найти:",
+ /* zh_hans */ "查找:",
+ /* zh_hant */ "尋找:",
+ ],
+ // SearchReplacementLabel (for input field)
+ [
+ /* en */ "Replace:",
+ /* de */ "Ersetzen:",
+ /* es */ "Reemplazar:",
+ /* fr */ "Remplacer:",
+ /* it */ "Sostituire:",
+ /* ja */ "置換:",
+ /* ko */ "바꾸기:",
+ /* pt_br */ "Substituir:",
+ /* ru */ "Замена:",
+ /* zh_hans */ "替换:",
+ /* zh_hant */ "替換:",
+ ],
+ // SearchMatchCase (toggle)
+ [
+ /* en */ "Match Case",
+ /* de */ "Groß/Klein",
+ /* es */ "May/Min",
+ /* fr */ "Casse",
+ /* it */ "Maius/minus",
+ /* ja */ "大/小文字",
+ /* ko */ "대소문자",
+ /* pt_br */ "Maius/minus",
+ /* ru */ "Регистр",
+ /* zh_hans */ "区分大小写",
+ /* zh_hant */ "區分大小寫",
+ ],
+ // SearchWholeWord (toggle)
+ [
+ /* en */ "Whole Word",
+ /* de */ "Ganzes Wort",
+ /* es */ "Palabra",
+ /* fr */ "Mot entier",
+ /* it */ "Parola",
+ /* ja */ "単語単位",
+ /* ko */ "전체 단어",
+ /* pt_br */ "Palavra",
+ /* ru */ "Слово",
+ /* zh_hans */ "全字匹配",
+ /* zh_hant */ "全字匹配",
+ ],
+ // SearchUseRegex (toggle)
+ [
+ /* en */ "Use Regex",
+ /* de */ "RegEx",
+ /* es */ "RegEx",
+ /* fr */ "RegEx",
+ /* it */ "RegEx",
+ /* ja */ "正規表現",
+ /* ko */ "정규식",
+ /* pt_br */ "RegEx",
+ /* ru */ "RegEx",
+ /* zh_hans */ "正则",
+ /* zh_hant */ "正則",
+ ],
+ // SearchReplaceAll (button)
+ [
+ /* en */ "Replace All",
+ /* de */ "Alle ersetzen",
+ /* es */ "Reemplazar todo",
+ /* fr */ "Remplacer tout",
+ /* it */ "Sostituisci tutto",
+ /* ja */ "すべて置換",
+ /* ko */ "모두 바꾸기",
+ /* pt_br */ "Substituir tudo",
+ /* ru */ "Заменить все",
+ /* zh_hans */ "全部替换",
+ /* zh_hant */ "全部取代",
+ ],
+ // SearchClose (button)
+ [
+ /* en */ "Close",
+ /* de */ "Schließen",
+ /* es */ "Cerrar",
+ /* fr */ "Fermer",
+ /* it */ "Chiudi",
+ /* ja */ "閉じる",
+ /* ko */ "닫기",
+ /* pt_br */ "Fechar",
+ /* ru */ "Закрыть",
+ /* zh_hans */ "关闭",
+ /* zh_hant */ "關閉",
+ ],
+
+ // EncodingReopen
+ [
+ /* en */ "Reopen with encoding",
+ /* de */ "Mit Kodierung erneut öffnen",
+ /* es */ "Reabrir con codificación",
+ /* fr */ "Rouvrir avec un encodage différent",
+ /* it */ "Riapri con codifica",
+ /* ja */ "エンコーディングで再度開く",
+ /* ko */ "인코딩으로 다시 열기",
+ /* pt_br */ "Reabrir com codificação",
+ /* ru */ "Открыть снова с кодировкой",
+ /* zh_hans */ "使用编码重新打开",
+ /* zh_hant */ "使用編碼重新打開",
+ ],
+ // EncodingConvert
+ [
+ /* en */ "Convert to encoding",
+ /* de */ "In Kodierung konvertieren",
+ /* es */ "Convertir a otra codificación",
+ /* fr */ "Convertir en encodage",
+ /* it */ "Converti in codifica",
+ /* ja */ "エンコーディングに変換",
+ /* ko */ "인코딩으로 변환",
+ /* pt_br */ "Converter para codificação",
+ /* ru */ "Преобразовать в кодировку",
+ /* zh_hans */ "转换为编码",
+ /* zh_hant */ "轉換為編碼",
+ ],
+
+ // IndentationTabs
+ [
+ /* en */ "Tabs",
+ /* de */ "Tabs",
+ /* es */ "Tabulaciones",
+ /* fr */ "Tabulations",
+ /* it */ "Tabulazioni",
+ /* ja */ "タブ",
+ /* ko */ "탭",
+ /* pt_br */ "Tabulações",
+ /* ru */ "Табы",
+ /* zh_hans */ "制表符",
+ /* zh_hant */ "製表符",
+ ],
+ // IndentationSpaces
+ [
+ /* en */ "Spaces",
+ /* de */ "Leerzeichen",
+ /* es */ "Espacios",
+ /* fr */ "Espaces",
+ /* it */ "Spazi",
+ /* ja */ "スペース",
+ /* ko */ "공백",
+ /* pt_br */ "Espaços",
+ /* ru */ "Пробелы",
+ /* zh_hans */ "空格",
+ /* zh_hant */ "空格",
+ ],
+
+ // SaveAsDialogPathLabel
+ [
+ /* en */ "Folder:",
+ /* de */ "Ordner:",
+ /* es */ "Carpeta:",
+ /* fr */ "Dossier:",
+ /* it */ "Cartella:",
+ /* ja */ "フォルダ:",
+ /* ko */ "폴더:",
+ /* pt_br */ "Pasta:",
+ /* ru */ "Папка:",
+ /* zh_hans */ "文件夹:",
+ /* zh_hant */ "資料夾:",
+ ],
+ // SaveAsDialogNameLabel
+ [
+ /* en */ "File name:",
+ /* de */ "Dateiname:",
+ /* es */ "Nombre de archivo:",
+ /* fr */ "Nom de fichier:",
+ /* it */ "Nome del file:",
+ /* ja */ "ファイル名:",
+ /* ko */ "파일 이름:",
+ /* pt_br */ "Nome do arquivo:",
+ /* ru */ "Имя файла:",
+ /* zh_hans */ "文件名:",
+ /* zh_hant */ "檔案名稱:",
+ ],
+
+ // FileOverwriteWarning
+ [
+ /* en */ "Confirm Save As",
+ /* de */ "Speichern unter bestätigen",
+ /* es */ "Confirmar Guardar como",
+ /* fr */ "Confirmer Enregistrer sous",
+ /* it */ "Conferma Salva con nome",
+ /* ja */ "名前を付けて保存の確認",
+ /* ko */ "다른 이름으로 저장 확인",
+ /* pt_br */ "Confirmar Salvar como",
+ /* ru */ "Подтвердите «Сохранить как…»",
+ /* zh_hans */ "确认另存为",
+ /* zh_hant */ "確認另存新檔",
+ ],
+ // FileOverwriteWarningDescription
+ [
+ /* en */ "File already exists. Do you want to overwrite it?",
+ /* de */ "Datei existiert bereits. Möchten Sie sie überschreiben?",
+ /* es */ "El archivo ya existe. ¿Desea sobrescribirlo?",
+ /* fr */ "Le fichier existe déjà. Voulez-vous l’écraser?",
+ /* it */ "Il file esiste già. Vuoi sovrascriverlo?",
+ /* ja */ "ファイルは既に存在します。上書きしますか?",
+ /* ko */ "파일이 이미 존재합니다. 덮어쓰시겠습니까?",
+ /* pt_br */ "O arquivo já existe. Deseja sobrescrevê-lo?",
+ /* ru */ "Файл уже существует. Перезаписать?",
+ /* zh_hans */ "文件已存在。要覆盖它吗?",
+ /* zh_hant */ "檔案已存在。要覆蓋它嗎?",
+ ],
+];
+
+static mut S_LANG: LangId = LangId::en;
+
+pub fn init() {
+ let scratch = scratch_arena(None);
+ let langs = sys::preferred_languages(&scratch);
+ let mut lang = LangId::en;
+
+ for l in langs {
+ lang = match l.as_str() {
+ "en" => LangId::en,
+ "de" => LangId::de,
+ "es" => LangId::es,
+ "fr" => LangId::fr,
+ "it" => LangId::it,
+ "ja" => LangId::ja,
+ "ko" => LangId::ko,
+ "pt-br" => LangId::pt_br,
+ "ru" => LangId::ru,
+ "zh-hant" => LangId::zh_hant,
+ "zh" => LangId::zh_hans,
+ _ => continue,
+ };
+ break;
+ }
+
+ unsafe {
+ S_LANG = lang;
+ }
+}
+
+pub fn loc(id: LocId) -> &'static str {
+ S_LANG_LUT[id as usize][unsafe { S_LANG as usize }]
+}
diff --git a/pkgs/edit/src/bin/edit/main.rs b/pkgs/edit/src/bin/edit/main.rs
new file mode 100644
index 0000000..2c84004
--- /dev/null
+++ b/pkgs/edit/src/bin/edit/main.rs
@@ -0,0 +1,611 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#![feature(let_chains, linked_list_cursors, os_string_truncate, string_from_utf8_lossy_owned)]
+
+mod documents;
+mod draw_editor;
+mod draw_filepicker;
+mod draw_menubar;
+mod draw_statusbar;
+mod localization;
+mod state;
+
+use std::borrow::Cow;
+#[cfg(feature = "debug-latency")]
+use std::fmt::Write;
+use std::path::Path;
+use std::{env, process};
+
+use draw_editor::*;
+use draw_filepicker::*;
+use draw_menubar::*;
+use draw_statusbar::*;
+use edit::arena::{self, ArenaString, scratch_arena};
+use edit::framebuffer::{self, IndexedColor};
+use edit::helpers::{KIBI, MEBI, MetricFormatter, Rect, Size};
+use edit::input::{self, kbmod, vk};
+use edit::oklab::oklab_blend;
+use edit::tui::*;
+use edit::vt::{self, Token};
+use edit::{apperr, arena_format, base64, path, sys};
+use localization::*;
+use state::*;
+
+#[cfg(target_pointer_width = "32")]
+const SCRATCH_ARENA_CAPACITY: usize = 128 * MEBI;
+#[cfg(target_pointer_width = "64")]
+const SCRATCH_ARENA_CAPACITY: usize = 512 * MEBI;
+
+fn main() -> process::ExitCode {
+ if cfg!(debug_assertions) {
+ let hook = std::panic::take_hook();
+ std::panic::set_hook(Box::new(move |info| {
+ drop(RestoreModes);
+ drop(sys::Deinit);
+ hook(info);
+ }));
+ }
+
+ match run() {
+ Ok(()) => process::ExitCode::SUCCESS,
+ Err(err) => {
+ sys::write_stdout(&format!("{}\r\n", FormatApperr::from(err)));
+ process::ExitCode::FAILURE
+ }
+ }
+}
+
+fn run() -> apperr::Result<()> {
+ // Init `sys` first, as everything else may depend on its functionality (IO, function pointers, etc.).
+ let _sys_deinit = sys::init()?;
+ // Next init `arena`, so that `scratch_arena` works. `loc` depends on it.
+ arena::init(SCRATCH_ARENA_CAPACITY)?;
+ // Init the `loc` module, so that error messages are localized.
+ localization::init();
+
+ let mut state = State::new()?;
+ if handle_args(&mut state)? {
+ return Ok(());
+ }
+
+ // sys::init() will switch the terminal to raw mode which prevents the user from pressing Ctrl+C.
+ // Since the `read_file` call may hang for some reason, we must only call this afterwards.
+ // `set_modes()` will enable mouse mode which is equally annoying to switch out for users
+ // and so we do it afterwards, for similar reasons.
+ sys::switch_modes()?;
+
+ let mut vt_parser = vt::Parser::new();
+ let mut input_parser = input::Parser::new();
+ let mut tui = Tui::new()?;
+
+ let _restore = setup_terminal(&mut tui, &mut vt_parser);
+
+ state.menubar_color_bg = oklab_blend(
+ tui.indexed(IndexedColor::Background),
+ tui.indexed_alpha(IndexedColor::BrightBlue, 1, 2),
+ );
+ state.menubar_color_fg = tui.contrasted(state.menubar_color_bg);
+ let floater_bg = oklab_blend(
+ tui.indexed_alpha(IndexedColor::Background, 2, 3),
+ tui.indexed_alpha(IndexedColor::Foreground, 1, 3),
+ );
+ let floater_fg = tui.contrasted(floater_bg);
+ tui.setup_modifier_translations(ModifierTranslations {
+ ctrl: loc(LocId::Ctrl),
+ alt: loc(LocId::Alt),
+ shift: loc(LocId::Shift),
+ });
+ tui.set_floater_default_bg(floater_bg);
+ tui.set_floater_default_fg(floater_fg);
+ tui.set_modal_default_bg(floater_bg);
+ tui.set_modal_default_fg(floater_fg);
+
+ sys::inject_window_size_into_stdin();
+
+ #[cfg(feature = "debug-latency")]
+ let mut last_latency_width = 0;
+
+ loop {
+ #[cfg(feature = "debug-latency")]
+ let time_beg;
+ #[cfg(feature = "debug-latency")]
+ let mut passes;
+
+ // Process a batch of input.
+ {
+ let scratch = scratch_arena(None);
+ let read_timeout = vt_parser.read_timeout().min(tui.read_timeout());
+ let Some(input) = sys::read_stdin(&scratch, read_timeout) else {
+ break;
+ };
+
+ #[cfg(feature = "debug-latency")]
+ {
+ time_beg = std::time::Instant::now();
+ passes = 0usize;
+ }
+
+ let vt_iter = vt_parser.parse(&input);
+ let mut input_iter = input_parser.parse(vt_iter);
+
+ while {
+ let input = input_iter.next();
+ let more = input.is_some();
+ let mut ctx = tui.create_context(input);
+
+ draw(&mut ctx, &mut state);
+
+ #[cfg(feature = "debug-latency")]
+ {
+ passes += 1;
+ }
+
+ more
+ } {}
+ }
+
+ // Continue rendering until the layout has settled.
+ // This can take >1 frame, if the input focus is tossed between different controls.
+ while tui.needs_settling() {
+ let mut ctx = tui.create_context(None);
+
+ draw(&mut ctx, &mut state);
+
+ #[cfg(feature = "debug-layout")]
+ {
+ drop(ctx);
+ state.buffer.buffer.copy_from_str(&tui.debug_layout());
+ }
+
+ #[cfg(feature = "debug-latency")]
+ {
+ passes += 1;
+ }
+ }
+
+ if state.exit {
+ break;
+ }
+
+ // Render the UI and write it to the terminal.
+ {
+ let scratch = scratch_arena(None);
+ let mut output = tui.render(&scratch);
+
+ {
+ let filename = state.documents.active().map_or("", |d| &d.filename);
+ if filename != state.osc_title_filename {
+ write_terminal_title(&mut output, filename);
+ state.osc_title_filename = filename.to_string();
+ }
+ }
+
+ if state.osc_clipboard_send_generation == tui.clipboard_generation() {
+ write_osc_clipboard(&mut output, &mut state, &tui);
+ }
+
+ #[cfg(feature = "debug-latency")]
+ {
+ // Print the number of passes and latency in the top right corner.
+ let time_end = std::time::Instant::now();
+ let status = time_end - time_beg;
+
+ let scratch_alt = scratch_arena(Some(&scratch));
+ let status = arena_format!(
+ &scratch_alt,
+ "{}P {}B {:.3}μs",
+ passes,
+ output.len(),
+ status.as_nanos() as f64 / 1000.0
+ );
+
+ // "μs" is 3 bytes and 2 columns.
+ let cols = status.len() as i32 - 3 + 2;
+
+ // Since the status may shrink and grow, we may have to overwrite the previous one with whitespace.
+ let padding = (last_latency_width - cols).max(0);
+
+ // If the `output` is already very large,
+ // Rust may double the size during the write below.
+ // Let's avoid that by reserving the needed size in advance.
+ output.reserve_exact(128);
+
+ // To avoid moving the cursor, push and pop it onto the VT cursor stack.
+ _ = write!(
+ output,
+ "\x1b7\x1b[0;41;97m\x1b[1;{0}H{1:2$}{3}\x1b8",
+ tui.size().width - cols - padding + 1,
+ "",
+ padding as usize,
+ status
+ );
+
+ last_latency_width = cols;
+ }
+
+ sys::write_stdout(&output);
+ }
+ }
+
+ Ok(())
+}
+
+// Returns true if the application should exit early.
+fn handle_args(state: &mut State) -> apperr::Result {
+ let mut cwd = env::current_dir()?;
+ let mut path = None;
+
+ // The best CLI argument parser in the world.
+ if let Some(arg) = env::args_os().nth(1) {
+ if arg == "-h" || arg == "--help" || (cfg!(windows) && arg == "/?") {
+ print_help();
+ return Ok(true);
+ } else if arg == "-v" || arg == "--version" {
+ print_version();
+ return Ok(true);
+ } else if arg == "-" {
+ // We'll check for a redirected stdin no matter what, so we can just ignore "-".
+ } else {
+ let p = cwd.join(Path::new(&arg));
+ let p = path::normalize(&p);
+ if let Some(parent) = p.parent() {
+ cwd = parent.to_path_buf();
+ }
+ path = Some(p);
+ }
+ }
+
+ if let Some(mut file) = sys::open_stdin_if_redirected() {
+ let doc = state.documents.add_untitled()?;
+ let mut tb = doc.buffer.borrow_mut();
+ tb.read_file(&mut file, None)?;
+ tb.mark_as_dirty();
+ } else if let Some(path) = path {
+ state.documents.add_file_path(&path)?;
+ } else {
+ state.documents.add_untitled()?;
+ }
+
+ state.file_picker_pending_dir = DisplayablePathBuf::new(cwd);
+ Ok(false)
+}
+
+fn print_help() {
+ sys::write_stdout(concat!(
+ "Usage: edit [OPTIONS] [FILE]\r\n",
+ "Options:\r\n",
+ " -h, --help Print this help message\r\n",
+ " -v, --version Print the version number\r\n",
+ ));
+}
+
+fn print_version() {
+ sys::write_stdout(concat!("edit version ", env!("CARGO_PKG_VERSION"), "\r\n"));
+}
+
+fn draw(ctx: &mut Context, state: &mut State) {
+ draw_menubar(ctx, state);
+ draw_editor(ctx, state);
+ draw_statusbar(ctx, state);
+
+ if state.wants_close {
+ draw_handle_wants_close(ctx, state);
+ }
+ if state.wants_exit {
+ draw_handle_wants_exit(ctx, state);
+ }
+ if state.wants_file_picker != StateFilePicker::None {
+ draw_file_picker(ctx, state);
+ }
+ if state.wants_save {
+ draw_handle_save(ctx, state);
+ }
+ if state.wants_encoding_change != StateEncodingChange::None {
+ draw_dialog_encoding_change(ctx, state);
+ }
+ if state.wants_document_picker {
+ draw_document_picker(ctx, state);
+ }
+ if state.wants_about {
+ draw_dialog_about(ctx, state);
+ }
+ if state.osc_clipboard_seen_generation != ctx.clipboard_generation() {
+ draw_handle_clipboard_change(ctx, state);
+ }
+ if state.error_log_count != 0 {
+ draw_error_log(ctx, state);
+ }
+
+ if let Some(key) = ctx.keyboard_input() {
+ // Shortcuts that are not handled as part of the textarea, etc.
+
+ if key == kbmod::CTRL | vk::N {
+ draw_add_untitled_document(ctx, state);
+ } else if key == kbmod::CTRL | vk::O {
+ state.wants_file_picker = StateFilePicker::Open;
+ } else if key == kbmod::CTRL | vk::S {
+ state.wants_save = true;
+ } else if key == kbmod::CTRL_SHIFT | vk::S {
+ state.wants_file_picker = StateFilePicker::SaveAs;
+ } else if key == kbmod::CTRL | vk::W {
+ state.wants_close = true;
+ } else if key == kbmod::CTRL | vk::P {
+ state.wants_document_picker = true;
+ } else if key == kbmod::CTRL | vk::Q {
+ state.wants_exit = true;
+ } else if key == kbmod::CTRL | vk::F && state.wants_search.kind != StateSearchKind::Disabled
+ {
+ state.wants_search.kind = StateSearchKind::Search;
+ state.wants_search.focus = true;
+ } else if key == kbmod::CTRL | vk::R && state.wants_search.kind != StateSearchKind::Disabled
+ {
+ state.wants_search.kind = StateSearchKind::Replace;
+ state.wants_search.focus = true;
+ } else {
+ return;
+ }
+
+ // All of the above shortcuts happen to require a rerender.
+ ctx.needs_rerender();
+ ctx.set_input_consumed();
+ }
+}
+
+fn draw_handle_wants_exit(_ctx: &mut Context, state: &mut State) {
+ while let Some(doc) = state.documents.active() {
+ if doc.buffer.borrow().is_dirty() {
+ state.wants_close = true;
+ return;
+ }
+ state.documents.remove_active();
+ }
+
+ if state.documents.len() == 0 {
+ state.exit = true;
+ }
+}
+
+#[cold]
+fn write_terminal_title(output: &mut ArenaString, filename: &str) {
+ output.push_str("\x1b]0;");
+
+ if !filename.is_empty() {
+ output.push_str(&sanitize_control_chars(filename));
+ output.push_str(" - ");
+ }
+
+ output.push_str("edit\x1b\\");
+}
+
+const LARGE_CLIPBOARD_THRESHOLD: usize = 4 * KIBI;
+
+fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) {
+ let generation = ctx.clipboard_generation();
+
+ if state.osc_clipboard_always_send || ctx.clipboard().len() < LARGE_CLIPBOARD_THRESHOLD {
+ state.osc_clipboard_seen_generation = generation;
+ state.osc_clipboard_send_generation = generation;
+ return;
+ }
+
+ let over_limit = ctx.clipboard().len() >= SCRATCH_ARENA_CAPACITY / 4;
+
+ ctx.modal_begin("warning", loc(LocId::WarningDialogTitle));
+ {
+ ctx.block_begin("description");
+ ctx.attr_padding(Rect::three(1, 2, 1));
+
+ if over_limit {
+ ctx.label("line1", loc(LocId::LargeClipboardWarningLine1));
+ ctx.attr_position(Position::Center);
+ ctx.label("line2", loc(LocId::SuperLargeClipboardWarning));
+ ctx.attr_position(Position::Center);
+ } else {
+ let label2 = {
+ let template = loc(LocId::LargeClipboardWarningLine2);
+ let size = arena_format!(ctx.arena(), "{}", MetricFormatter(ctx.clipboard().len()));
+
+ let mut label =
+ ArenaString::with_capacity_in(template.len() + size.len(), ctx.arena());
+ label.push_str(template);
+ label.replace_once_in_place("{size}", &size);
+ label
+ };
+
+ ctx.label("line1", loc(LocId::LargeClipboardWarningLine1));
+ ctx.attr_position(Position::Center);
+ ctx.label("line2", &label2);
+ ctx.attr_position(Position::Center);
+ ctx.label("line3", loc(LocId::LargeClipboardWarningLine3));
+ ctx.attr_position(Position::Center);
+ }
+ ctx.block_end();
+
+ ctx.table_begin("choices");
+ ctx.inherit_focus();
+ ctx.attr_padding(Rect::three(0, 2, 1));
+ ctx.attr_position(Position::Center);
+ ctx.table_set_cell_gap(Size { width: 2, height: 0 });
+ {
+ ctx.table_next_row();
+ ctx.inherit_focus();
+
+ if over_limit {
+ if ctx.button("ok", loc(LocId::Ok)) {
+ state.osc_clipboard_seen_generation = generation;
+ }
+ ctx.inherit_focus();
+ } else {
+ if ctx.button("always", loc(LocId::Always)) {
+ state.osc_clipboard_always_send = true;
+ state.osc_clipboard_seen_generation = generation;
+ state.osc_clipboard_send_generation = generation;
+ }
+
+ if ctx.button("yes", loc(LocId::Yes)) {
+ state.osc_clipboard_seen_generation = generation;
+ state.osc_clipboard_send_generation = generation;
+ }
+ if ctx.clipboard().len() < 10 * LARGE_CLIPBOARD_THRESHOLD {
+ ctx.inherit_focus();
+ }
+
+ if ctx.button("no", loc(LocId::No)) {
+ state.osc_clipboard_seen_generation = generation;
+ }
+ if ctx.clipboard().len() >= 10 * LARGE_CLIPBOARD_THRESHOLD {
+ ctx.inherit_focus();
+ }
+ }
+ }
+ ctx.table_end();
+ }
+ if ctx.modal_end() {
+ state.osc_clipboard_seen_generation = generation;
+ }
+}
+
+#[cold]
+fn write_osc_clipboard(output: &mut ArenaString, state: &mut State, tui: &Tui) {
+ let clipboard = tui.clipboard();
+ if !clipboard.is_empty() {
+ // Rust doubles the size of a string when it needs to grow it.
+ // If `clipboard` is *really* large, this may then double
+ // the size of the `output` from e.g. 100MB to 200MB. Not good.
+ // We can avoid that by reserving the needed size in advance.
+ output.reserve_exact(base64::encode_len(clipboard.len()) + 16);
+ output.push_str("\x1b]52;c;");
+ base64::encode(output, clipboard);
+ output.push_str("\x1b\\");
+ }
+ state.osc_clipboard_send_generation = tui.clipboard_generation().wrapping_sub(1);
+}
+
+struct RestoreModes;
+
+impl Drop for RestoreModes {
+ fn drop(&mut self) {
+ // Same as in the beginning but in the reverse order.
+ // It also includes DECSCUSR 0 to reset the cursor style and DECTCEM to show the cursor.
+ sys::write_stdout("\x1b[0 q\x1b[?25h\x1b]0;\x07\x1b[?1002;1006;2004l\x1b[?1049l");
+ }
+}
+
+fn setup_terminal(tui: &mut Tui, vt_parser: &mut vt::Parser) -> RestoreModes {
+ sys::write_stdout(concat!(
+ // 1049: Alternative Screen Buffer
+ // I put the ASB switch in the beginning, just in case the terminal performs
+ // some additional state tracking beyond the modes we enable/disable.
+ // 1002: Cell Motion Mouse Tracking
+ // 1006: SGR Mouse Mode
+ // 2004: Bracketed Paste Mode
+ "\x1b[?1049h\x1b[?1002;1006;2004h",
+ // OSC 4 color table requests for indices 0 through 15 (base colors).
+ "\x1b]4;0;?;1;?;2;?;3;?;4;?;5;?;6;?;7;?\x07",
+ "\x1b]4;8;?;9;?;10;?;11;?;12;?;13;?;14;?;15;?\x07",
+ // OSC 10 and 11 queries for the current foreground and background colors.
+ "\x1b]10;?\x07\x1b]11;?\x07",
+ // CSI c reports the terminal capabilities.
+ // It also helps us to detect the end of the responses, because not all
+ // terminals support the OSC queries, but all of them support CSI c.
+ "\x1b[c",
+ ));
+
+ let mut done = false;
+ let mut osc_buffer = String::new();
+ let mut indexed_colors = framebuffer::DEFAULT_THEME;
+ let mut color_responses = 0;
+
+ while !done {
+ let scratch = scratch_arena(None);
+ let Some(input) = sys::read_stdin(&scratch, vt_parser.read_timeout()) else {
+ break;
+ };
+
+ let mut vt_stream = vt_parser.parse(&input);
+ while let Some(token) = vt_stream.next() {
+ match token {
+ Token::Csi(state) if state.final_byte == 'c' => done = true,
+ Token::Osc { mut data, partial } => {
+ if partial {
+ osc_buffer.push_str(data);
+ continue;
+ }
+ if !osc_buffer.is_empty() {
+ osc_buffer.push_str(data);
+ data = &osc_buffer;
+ }
+
+ let mut splits = data.split_terminator(';');
+
+ let color = match splits.next().unwrap_or("") {
+ // The response is `4;;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 = rgb | 0xff000000;
+ color_responses += 1;
+ osc_buffer.clear();
+ }
+ _ => {}
+ }
+ }
+ }
+
+ if color_responses == indexed_colors.len() {
+ tui.setup_indexed_colors(indexed_colors);
+ }
+
+ RestoreModes
+}
+
+/// Strips all C0 control characters from the string an replaces them with "_".
+///
+/// Jury is still out on whether this should also strip C1 control characters.
+/// That requires parsing UTF8 codepoints, which is annoying.
+fn sanitize_control_chars(text: &str) -> Cow<'_, str> {
+ if let Some(off) = text.bytes().position(|b| (..0x20).contains(&b)) {
+ let mut sanitized = text.to_string();
+ // SAFETY: We only search for ASCII and replace it with ASCII.
+ let vec = unsafe { sanitized.as_bytes_mut() };
+
+ for i in &mut vec[off..] {
+ *i = if (..0x20).contains(i) { b'_' } else { *i }
+ }
+
+ Cow::Owned(sanitized)
+ } else {
+ Cow::Borrowed(text)
+ }
+}
diff --git a/pkgs/edit/src/bin/edit/state.rs b/pkgs/edit/src/bin/edit/state.rs
new file mode 100644
index 0000000..d23be30
--- /dev/null
+++ b/pkgs/edit/src/bin/edit/state.rs
@@ -0,0 +1,254 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use std::borrow::Cow;
+use std::ffi::{OsStr, OsString};
+use std::mem;
+use std::path::{Path, PathBuf};
+
+use edit::buffer::TextBuffer;
+use edit::framebuffer::IndexedColor;
+use edit::helpers::*;
+use edit::tui::*;
+use edit::{apperr, buffer, icu, sys};
+
+use crate::documents::DocumentManager;
+use crate::localization::*;
+
+#[repr(transparent)]
+pub struct FormatApperr(apperr::Error);
+
+impl From for FormatApperr {
+ fn from(err: apperr::Error) -> Self {
+ FormatApperr(err)
+ }
+}
+
+impl std::fmt::Display for FormatApperr {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self.0 {
+ apperr::APP_ICU_MISSING => f.write_str(loc(LocId::ErrorIcuMissing)),
+ apperr::Error::App(code) => write!(f, "Unknown app error code: {code}"),
+ apperr::Error::Icu(code) => icu::apperr_format(f, code),
+ apperr::Error::Sys(code) => sys::apperr_format(f, code),
+ }
+ }
+}
+
+pub struct DisplayablePathBuf {
+ value: PathBuf,
+ str: Cow<'static, str>,
+}
+
+impl DisplayablePathBuf {
+ pub fn new(value: PathBuf) -> Self {
+ let str = value.to_string_lossy();
+ let str = unsafe { mem::transmute::, Cow<'_, str>>(str) };
+ Self { value, str }
+ }
+
+ pub fn as_path(&self) -> &Path {
+ &self.value
+ }
+
+ pub fn as_str(&self) -> &str {
+ &self.str
+ }
+
+ pub fn as_bytes(&self) -> &[u8] {
+ self.value.as_os_str().as_encoded_bytes()
+ }
+}
+
+impl Default for DisplayablePathBuf {
+ fn default() -> Self {
+ Self { value: Default::default(), str: Cow::Borrowed("") }
+ }
+}
+
+impl Clone for DisplayablePathBuf {
+ fn clone(&self) -> Self {
+ DisplayablePathBuf::new(self.value.clone())
+ }
+}
+
+impl From for DisplayablePathBuf {
+ fn from(s: OsString) -> DisplayablePathBuf {
+ DisplayablePathBuf::new(PathBuf::from(s))
+ }
+}
+
+impl> From<&T> for DisplayablePathBuf {
+ fn from(s: &T) -> DisplayablePathBuf {
+ DisplayablePathBuf::new(PathBuf::from(s))
+ }
+}
+
+pub struct StateSearch {
+ pub kind: StateSearchKind,
+ pub focus: bool,
+}
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum StateSearchKind {
+ Hidden,
+ Disabled,
+ Search,
+ Replace,
+}
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum StateFilePicker {
+ None,
+ Open,
+ SaveAs,
+
+ SaveAsShown, // Transitioned from SaveAs
+}
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum StateEncodingChange {
+ None,
+ Convert,
+ Reopen,
+}
+
+pub struct State {
+ pub menubar_color_bg: u32,
+ pub menubar_color_fg: u32,
+
+ pub documents: DocumentManager,
+
+ // A ring buffer of the last 10 errors.
+ pub error_log: [String; 10],
+ pub error_log_index: usize,
+ pub error_log_count: usize,
+
+ pub wants_file_picker: StateFilePicker,
+ pub file_picker_pending_dir: DisplayablePathBuf,
+ pub file_picker_pending_name: PathBuf,
+ pub file_picker_entries: Option>,
+ pub file_picker_overwrite_warning: Option, // The path the warning is about.
+
+ pub wants_search: StateSearch,
+ pub search_needle: String,
+ pub search_replacement: String,
+ pub search_options: buffer::SearchOptions,
+ pub search_success: bool,
+
+ pub wants_save: bool,
+ pub wants_statusbar_focus: bool,
+ pub wants_encoding_picker: bool,
+ pub wants_encoding_change: StateEncodingChange,
+ pub wants_indentation_picker: bool,
+ pub wants_document_picker: bool,
+ pub wants_about: bool,
+ pub wants_close: bool,
+ pub wants_exit: bool,
+
+ pub osc_title_filename: String,
+ pub osc_clipboard_seen_generation: u32,
+ pub osc_clipboard_send_generation: u32,
+ pub osc_clipboard_always_send: bool,
+ pub exit: bool,
+}
+
+impl State {
+ pub fn new() -> apperr::Result {
+ let buffer = TextBuffer::new_rc(false)?;
+ {
+ let mut tb = buffer.borrow_mut();
+ tb.set_margin_enabled(true);
+ tb.set_line_highlight_enabled(true);
+ }
+
+ Ok(Self {
+ menubar_color_bg: 0,
+ menubar_color_fg: 0,
+
+ documents: Default::default(),
+
+ error_log: [const { String::new() }; 10],
+ error_log_index: 0,
+ error_log_count: 0,
+
+ wants_file_picker: StateFilePicker::None,
+ file_picker_pending_dir: Default::default(),
+ file_picker_pending_name: Default::default(),
+ file_picker_entries: None,
+ file_picker_overwrite_warning: None,
+
+ wants_search: StateSearch { kind: StateSearchKind::Hidden, focus: false },
+ search_needle: Default::default(),
+ search_replacement: Default::default(),
+ search_options: Default::default(),
+ search_success: true,
+
+ wants_save: false,
+ wants_statusbar_focus: false,
+ wants_encoding_picker: false,
+ wants_encoding_change: StateEncodingChange::None,
+ wants_indentation_picker: false,
+ wants_document_picker: false,
+ wants_about: false,
+ wants_close: false,
+ wants_exit: false,
+
+ osc_title_filename: Default::default(),
+ osc_clipboard_seen_generation: 0,
+ osc_clipboard_send_generation: 0,
+ osc_clipboard_always_send: false,
+ exit: false,
+ })
+ }
+}
+
+pub fn draw_add_untitled_document(ctx: &mut Context, state: &mut State) {
+ if let Err(err) = state.documents.add_untitled() {
+ error_log_add(ctx, state, err);
+ }
+}
+
+pub fn error_log_add(ctx: &mut Context, state: &mut State, err: apperr::Error) {
+ let msg = format!("{}", FormatApperr::from(err));
+ if !msg.is_empty() {
+ state.error_log[state.error_log_index] = msg;
+ state.error_log_index = (state.error_log_index + 1) % state.error_log.len();
+ state.error_log_count = state.error_log.len().min(state.error_log_count + 1);
+ ctx.needs_rerender();
+ }
+}
+
+pub fn draw_error_log(ctx: &mut Context, state: &mut State) {
+ ctx.modal_begin("error", loc(LocId::ErrorDialogTitle));
+ ctx.attr_background_rgba(ctx.indexed(IndexedColor::Red));
+ ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::BrightWhite));
+ {
+ ctx.block_begin("content");
+ ctx.attr_padding(Rect::three(0, 2, 1));
+ {
+ let off = state.error_log_index + state.error_log.len() - state.error_log_count;
+
+ for i in 0..state.error_log_count {
+ let idx = (off + i) % state.error_log.len();
+ let msg = &state.error_log[idx][..];
+
+ if !msg.is_empty() {
+ ctx.next_block_id_mixin(i as u64);
+ ctx.label("error", msg);
+ ctx.attr_overflow(Overflow::TruncateTail);
+ }
+ }
+ }
+ ctx.block_end();
+
+ if ctx.button("ok", loc(LocId::Ok)) {
+ state.error_log_count = 0;
+ }
+ ctx.attr_position(Position::Center);
+ ctx.inherit_focus();
+ }
+ if ctx.modal_end() {
+ state.error_log_count = 0;
+ }
+}
diff --git a/pkgs/edit/src/buffer/gap_buffer.rs b/pkgs/edit/src/buffer/gap_buffer.rs
new file mode 100644
index 0000000..58425cd
--- /dev/null
+++ b/pkgs/edit/src/buffer/gap_buffer.rs
@@ -0,0 +1,377 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use std::ops::Range;
+use std::ptr::{self, NonNull};
+use std::slice;
+
+use crate::document::{ReadableDocument, WriteableDocument};
+use crate::helpers::*;
+use crate::{apperr, sys};
+
+#[cfg(target_pointer_width = "32")]
+const LARGE_CAPACITY: usize = 128 * MEBI;
+#[cfg(target_pointer_width = "64")]
+const LARGE_CAPACITY: usize = 4 * GIBI;
+const LARGE_ALLOC_CHUNK: usize = 64 * KIBI;
+const LARGE_GAP_CHUNK: usize = 4 * KIBI;
+
+const SMALL_CAPACITY: usize = 128 * KIBI;
+const SMALL_ALLOC_CHUNK: usize = 256;
+const SMALL_GAP_CHUNK: usize = 16;
+
+// TODO: Instead of having a specialization for small buffers here,
+// tui.rs could also just keep a MRU set of large buffers around.
+enum BackingBuffer {
+ VirtualMemory(NonNull, usize),
+ Vec(Vec),
+}
+
+impl Drop for BackingBuffer {
+ fn drop(&mut self) {
+ unsafe {
+ if let BackingBuffer::VirtualMemory(ptr, reserve) = *self {
+ sys::virtual_release(ptr, reserve);
+ }
+ }
+ }
+}
+
+/// Most people know how Vec 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) -> apperr::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 { sys::virtual_reserve(reserve)? };
+ buffer = BackingBuffer::VirtualMemory(text, reserve);
+ }
+
+ Ok(Self {
+ text,
+ reserve,
+ commit: 0,
+ text_length: 0,
+ gap_off: 0,
+ gap_len: 0,
+ generation: 0,
+ buffer,
+ })
+ }
+
+ #[allow(clippy::len_without_is_empty)]
+ pub fn len(&self) -> usize {
+ self.text_length
+ }
+
+ pub fn generation(&self) -> u32 {
+ self.generation
+ }
+
+ pub fn set_generation(&mut self, generation: u32) {
+ self.generation = generation;
+ }
+
+ /// WARNING: The returned slice must not necessarily be the same length as `len` (due to OOM).
+ pub fn allocate_gap(&mut self, off: usize, len: usize, delete: usize) -> &mut [u8] {
+ // Sanitize parameters
+ let off = off.min(self.text_length);
+ let delete = delete.min(self.text_length - off);
+
+ // Move the existing gap if it exists
+ if off != self.gap_off {
+ self.move_gap(off);
+ }
+
+ // Delete the text
+ if delete > 0 {
+ self.delete_text(delete);
+ }
+
+ // Enlarge the gap if needed
+ if len > self.gap_len {
+ self.enlarge_gap(len);
+ }
+
+ self.generation = self.generation.wrapping_add(1);
+ unsafe { slice::from_raw_parts_mut(self.text.add(self.gap_off).as_ptr(), self.gap_len) }
+ }
+
+ fn move_gap(&mut self, off: usize) {
+ if self.gap_len > 0 {
+ //
+ // v gap_off
+ // left: |ABCDEFGHIJKLMN OPQRSTUVWXYZ|
+ // |ABCDEFGHI JKLMNOPQRSTUVWXYZ|
+ // ^ off
+ // move: JKLMN
+ //
+ // v gap_off
+ // !left: |ABCDEFGHIJKLMN OPQRSTUVWXYZ|
+ // |ABCDEFGHIJKLMNOPQRS TUVWXYZ|
+ // ^ off
+ // move: OPQRS
+ //
+ let left = off < self.gap_off;
+ let move_src = if left { off } else { self.gap_off + self.gap_len };
+ let move_dst = if left { off + self.gap_len } else { self.gap_off };
+ let move_len = if left { self.gap_off - off } else { off - self.gap_off };
+
+ unsafe { self.text.add(move_src).copy_to(self.text.add(move_dst), move_len) };
+
+ if cfg!(debug_assertions) {
+ // Fill the moved-out bytes with 0xCD to make debugging easier.
+ unsafe { self.text.add(off).write_bytes(0xCD, self.gap_len) };
+ }
+ }
+
+ self.gap_off = off;
+ }
+
+ fn delete_text(&mut self, delete: usize) {
+ if cfg!(debug_assertions) {
+ // Fill the deleted bytes with 0xCD to make debugging easier.
+ unsafe { self.text.add(self.gap_off + self.gap_len).write_bytes(0xCD, delete) };
+ }
+
+ self.gap_len += delete;
+ self.text_length -= delete;
+ }
+
+ fn enlarge_gap(&mut self, len: usize) {
+ let gap_chunk;
+ let alloc_chunk;
+
+ if matches!(self.buffer, BackingBuffer::VirtualMemory(..)) {
+ gap_chunk = LARGE_GAP_CHUNK;
+ alloc_chunk = LARGE_ALLOC_CHUNK;
+ } else {
+ gap_chunk = SMALL_GAP_CHUNK;
+ alloc_chunk = SMALL_ALLOC_CHUNK;
+ }
+
+ let gap_len_old = self.gap_len;
+ let gap_len_new = (len + gap_chunk + gap_chunk - 1) & !(gap_chunk - 1);
+
+ let bytes_old = self.commit;
+ let bytes_new = self.text_length + gap_len_new;
+
+ if bytes_new > bytes_old {
+ let bytes_new = (bytes_new + alloc_chunk - 1) & !(alloc_chunk - 1);
+
+ if bytes_new > self.reserve {
+ return;
+ }
+
+ match &mut self.buffer {
+ BackingBuffer::VirtualMemory(ptr, _) => unsafe {
+ if sys::virtual_commit(ptr.add(bytes_old), bytes_new - bytes_old).is_err() {
+ return;
+ }
+ },
+ BackingBuffer::Vec(v) => {
+ v.resize(bytes_new, 0);
+ self.text = unsafe { NonNull::new_unchecked(v.as_mut_ptr()) };
+ }
+ }
+
+ self.commit = bytes_new;
+ }
+
+ let gap_beg = unsafe { self.text.add(self.gap_off) };
+ unsafe {
+ ptr::copy(
+ gap_beg.add(gap_len_old).as_ptr(),
+ gap_beg.add(gap_len_new).as_ptr(),
+ self.text_length - self.gap_off,
+ )
+ };
+
+ if cfg!(debug_assertions) {
+ // Fill the moved-out bytes with 0xCD to make debugging easier.
+ unsafe { gap_beg.add(gap_len_old).write_bytes(0xCD, gap_len_new - gap_len_old) };
+ }
+
+ self.gap_len = gap_len_new;
+ }
+
+ pub fn commit_gap(&mut self, len: usize) {
+ assert!(len <= self.gap_len);
+ self.text_length += len;
+ self.gap_off += len;
+ self.gap_len -= len;
+ }
+
+ pub fn replace(&mut self, range: Range, src: &[u8]) {
+ let gap = self.allocate_gap(range.start, src.len(), range.end.saturating_sub(range.start));
+ let len = slice_copy_safe(gap, src);
+ self.commit_gap(len);
+ }
+
+ pub fn clear(&mut self) {
+ self.gap_off = 0;
+ self.gap_len += self.text_length;
+ self.generation = self.generation.wrapping_add(1);
+ self.text_length = 0;
+ }
+
+ pub fn extract_raw(
+ &self,
+ mut beg: usize,
+ mut end: usize,
+ out: &mut Vec,
+ mut out_off: usize,
+ ) {
+ debug_assert!(beg <= end && end <= self.text_length);
+
+ end = end.min(self.text_length);
+ beg = beg.min(end);
+ out_off = out_off.min(out.len());
+
+ if beg >= end {
+ return;
+ }
+
+ out.reserve(end - beg);
+
+ while beg < end {
+ let chunk = self.read_forward(beg);
+ let chunk = &chunk[..chunk.len().min(end - beg)];
+ out.replace_range(out_off..out_off, chunk);
+ beg += chunk.len();
+ out_off += chunk.len();
+ }
+ }
+
+ /// Replaces the entire buffer contents with the given `text`.
+ /// The method is optimized for the case where the given `text` already matches
+ /// the existing contents. Returns `true` if the buffer contents were changed.
+ pub fn copy_from(&mut self, src: &dyn ReadableDocument) -> bool {
+ let mut off = 0;
+
+ // Find the position at which the contents change.
+ loop {
+ let dst_chunk = self.read_forward(off);
+ let src_chunk = src.read_forward(off);
+
+ let dst_len = dst_chunk.len();
+ let src_len = src_chunk.len();
+ let len = dst_len.min(src_len);
+ let mismatch = dst_chunk[..len] != src_chunk[..len];
+
+ if mismatch {
+ break; // The contents differ.
+ }
+ if len == 0 {
+ if dst_len == src_len {
+ return false; // Both done simultaneously. -> Done.
+ }
+ break; // One of the two is shorter.
+ }
+
+ off += len;
+ }
+
+ // Update the buffer starting at `off`.
+ loop {
+ let chunk = src.read_forward(off);
+ self.replace(off..usize::MAX, chunk);
+ off += chunk.len();
+
+ // No more data to copy -> Done. By checking this _after_ the replace()
+ // call, we ensure that the initial `off..usize::MAX` range is deleted.
+ // This fixes going from some buffer contents to being empty.
+ if chunk.is_empty() {
+ return true;
+ }
+ }
+ }
+
+ /// Copies the contents of the buffer into a string.
+ pub fn copy_into(&self, dst: &mut dyn WriteableDocument) {
+ let mut beg = 0;
+ let mut off = 0;
+
+ while {
+ let chunk = self.read_forward(off);
+
+ // The first write will be 0..usize::MAX and effectively clear() the destination.
+ // Every subsequent write will be usize::MAX..usize::MAX and thus effectively append().
+ dst.replace(beg..usize::MAX, chunk);
+ beg = usize::MAX;
+
+ off += chunk.len();
+ off < self.text_length
+ } {}
+ }
+}
+
+impl ReadableDocument for GapBuffer {
+ fn read_forward(&self, off: usize) -> &[u8] {
+ let off = off.min(self.text_length);
+ let beg;
+ let len;
+
+ if off < self.gap_off {
+ // Cursor is before the gap: We can read until the start of the gap.
+ beg = off;
+ len = self.gap_off - off;
+ } else {
+ // Cursor is after the gap: We can read until the end of the buffer.
+ beg = off + self.gap_len;
+ len = self.text_length - off;
+ }
+
+ unsafe { slice::from_raw_parts(self.text.add(beg).as_ptr(), len) }
+ }
+
+ fn read_backward(&self, off: usize) -> &[u8] {
+ let off = off.min(self.text_length);
+ let beg;
+ let len;
+
+ if off <= self.gap_off {
+ // Cursor is before the gap: We can read until the beginning of the buffer.
+ beg = 0;
+ len = off;
+ } else {
+ // Cursor is after the gap: We can read until the end of the gap.
+ beg = self.gap_off + self.gap_len;
+ // The cursor_off doesn't account of the gap_len.
+ // (This allows us to move the gap without recalculating the cursor position.)
+ len = off - self.gap_off;
+ }
+
+ unsafe { slice::from_raw_parts(self.text.add(beg).as_ptr(), len) }
+ }
+}
diff --git a/pkgs/edit/src/buffer/mod.rs b/pkgs/edit/src/buffer/mod.rs
new file mode 100644
index 0000000..5587cae
--- /dev/null
+++ b/pkgs/edit/src/buffer/mod.rs
@@ -0,0 +1,2418 @@
+// 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::LinkedList;
+use std::fmt::Write as _;
+use std::fs::File;
+use std::io::{Read as _, Write as _};
+use std::mem::{self, MaybeUninit};
+use std::ops::Range;
+use std::rc::Rc;
+use std::str;
+
+use gap_buffer::GapBuffer;
+
+use crate::arena::{ArenaString, scratch_arena};
+use crate::cell::SemiRefCell;
+use crate::document::{ReadableDocument, WriteableDocument};
+use crate::framebuffer::{Framebuffer, IndexedColor};
+use crate::helpers::*;
+use crate::oklab::oklab_blend;
+use crate::simd::memchr2;
+use crate::unicode::{self, Cursor, MeasurementConfig};
+use crate::{apperr, icu};
+
+/// 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;
+
+/// 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.
+ 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,
+}
+
+/// 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,
+}
+
+/// Char- or word-wise navigation? Your choice.
+pub enum CursorMovement {
+ Grapheme,
+ Word,
+}
+
+/// 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: LinkedList>,
+ redo_stack: LinkedList>,
+ last_history_type: HistoryType,
+ last_save_generation: u32,
+
+ 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,
+ 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) -> apperr::Result {
+ let buffer = TextBuffer::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) -> apperr::Result {
+ Ok(Self {
+ buffer: GapBuffer::new(small)?,
+
+ undo_stack: LinkedList::new(),
+ redo_stack: LinkedList::new(),
+ last_history_type: HistoryType::Other,
+ last_save_generation: 0,
+
+ 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), // Unfortunately Windows users insist on CRLF
+ 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 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) = unicode::newlines_forward(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;
+ }
+
+ /// 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);
+ 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);
+ 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);
+ true
+ }
+ }
+
+ /// 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;
+ }
+
+ fn reflow(&mut self, force: bool) {
+ // +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.
+ let word_wrap_column =
+ if self.word_wrap_enabled && text_width >= 2 { text_width } else { 0 };
+
+ if force || self.word_wrap_column > word_wrap_column {
+ self.word_wrap_column = word_wrap_column;
+
+ if self.cursor.offset != 0 {
+ self.cursor = self
+ .cursor_move_to_logical_internal(Default::default(), self.cursor.logical_pos);
+ }
+
+ // Recalculate the line statistics.
+ if self.word_wrap_enabled {
+ 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;
+ }
+ }
+
+ self.cursor_for_rendering = None;
+ }
+
+ /// 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.cursor_for_rendering = None;
+ self.set_selection(None);
+ self.search = None;
+ self.mark_as_clean();
+ self.reflow(true);
+ }
+
+ /// 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>,
+ ) -> apperr::Result<()> {
+ let scratch = scratch_arena(None);
+ let mut buf = scratch.alloc_uninit().transpose();
+ 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, &mut buf, first_chunk_len, done)?;
+ } else {
+ self.read_file_with_icu(file, &mut buf, first_chunk_len, done)?;
+ }
+
+ // Figure out
+ // * the logical line count
+ // * the newline type (LF or CRLF)
+ // * the indentation type (tabs or spaces)
+ {
+ 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) = unicode::newlines_forward(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.
+ let newlines_are_crlf = 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) = unicode::newlines_forward(chunk, offset, lines, CoordType::MAX);
+ }
+
+ // 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.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,
+ ) -> apperr::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,
+ ) -> apperr::Result<()> {
+ 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) -> apperr::Result<()> {
+ 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) -> apperr::Result<()> {
+ 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);
+ if chunk.is_empty() {
+ break;
+ }
+
+ 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;
+ }
+
+ 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;
+ self.selection_generation = self.selection_generation.wrapping_add(1);
+ self.selection_generation
+ }
+
+ /// 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) -> apperr::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: &str,
+ ) -> apperr::Result<()> {
+ // Editors traditionally replace the previous search hit, not the next possible one.
+ if let (Some(search), Some(..)) = (&mut self.search, &self.selection) {
+ let search = search.get_mut();
+ if search.selection_generation == self.selection_generation {
+ self.write(replacement.as_bytes(), 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: &str,
+ ) -> apperr::Result<()> {
+ let replacement = replacement.as_bytes();
+ let mut search = self.find_construct_search(pattern, options)?;
+ let mut offset = 0;
+
+ loop {
+ self.find_select_next(&mut search, offset, false);
+ if !self.has_selection() {
+ break;
+ }
+ self.write(replacement, true);
+ offset = self.cursor.offset;
+ }
+
+ Ok(())
+ }
+
+ fn find_construct_search(
+ &self,
+ pattern: &str,
+ options: SearchOptions,
+ ) -> apperr::Result {
+ 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(&search.text) };
+ search.buffer_generation = self.buffer.generation();
+ }
+
+ 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 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) = unicode::newlines_forward(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) =
+ unicode::newlines_backward(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 preceeding 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.logical_pos.y || pos.x < cursor.logical_pos.x {
+ cursor = self.goto_line_start(cursor, pos.y);
+ }
+ } else {
+ // `goto_visual()` can only seek foward, 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()
+ && cursor.logical_pos.x >= 0
+ && cursor.logical_pos.y >= 0
+ && cursor.logical_pos.y <= self.stats.logical_lines
+ && cursor.visual_pos.x >= 0
+ && (self.word_wrap_column <= 0 || cursor.visual_pos.x <= self.word_wrap_column)
+ && cursor.visual_pos.y >= 0
+ && 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 scratch = scratch_arena(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 line = ArenaString::new_in(&scratch);
+ 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),
+ };
+
+ line.reserve(width as usize * 2);
+
+ for y in 0..height {
+ line.clear();
+
+ 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(&MARGIN_TEMPLATE[off..]);
+ } else if self.word_wrap_column <= 0 || cursor_beg.logical_pos.x == 0 {
+ // Regular line? Place "123 | " in the margin.
+ _ = write!(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;
+ _ = write!(
+ 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),
+ );
+ }
+ }
+
+ // 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(&TAB_WHITESPACE[..overlap as usize]);
+ cursor_beg = cursor_next;
+ }
+ }
+
+ fn find_control_char(text: &[u8], mut offset: usize) -> usize {
+ while offset < text.len() && (text[offset] >= 0x20 && text[offset] != 0x7f) {
+ offset += 1;
+ }
+ offset
+ }
+
+ let mut global_off = cursor_beg.offset;
+ let mut cursor_tab = 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 chunk_off = 0;
+ while chunk_off < chunk.len() {
+ let beg = chunk_off;
+ chunk_off = find_control_char(chunk, beg);
+
+ for chunk in chunk[beg..chunk_off].utf8_chunks() {
+ if !chunk.valid().is_empty() {
+ line.push_str(chunk.valid());
+ }
+ if !chunk.invalid().is_empty() {
+ line.push('\u{FFFD}');
+ }
+ }
+
+ while chunk_off < chunk.len()
+ && (chunk[chunk_off] < 0x20 || chunk[chunk_off] == 0x7f)
+ {
+ let ch = chunk[chunk_off];
+ chunk_off += 1;
+
+ if ch == b'\t' {
+ cursor_tab = self.cursor_move_to_offset_internal(
+ cursor_tab,
+ global_off + chunk_off - 1,
+ );
+ let tab_size = self.tab_size - (cursor_tab.column % self.tab_size);
+ line.push_str(&TAB_WHITESPACE[..tab_size as usize]);
+
+ // Since we know that we just aligned ourselves to the next tab stop,
+ // we can trivially process any successive tabs.
+ while chunk_off < chunk.len() && chunk[chunk_off] == b'\t' {
+ line.push_str(&TAB_WHITESPACE[..self.tab_size as usize]);
+ chunk_off += 1;
+ }
+ continue;
+ }
+
+ visualizer_buf[2] = if ch == 0x7F {
+ 0xA1 // U+2421
+ } else {
+ 0x80 | ch // 0x00..=0x1F => U+2400..=U+241F
+ };
+ // Our manually constructed UTF8 is never going to be invalid. Trust.
+ line.push_str(unsafe { str::from_utf8_unchecked(&visualizer_buf) });
+ }
+ }
+
+ 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);
+
+ // Draw the selection on this line, if any.
+ // FYI: `cursor_beg.visual_pos.y == visual_line` is necessary as the `visual_line`
+ // may be past the end of the document, and so it may not receive a highlight.
+ if cursor_beg.visual_pos.y == visual_line
+ && selection_beg <= cursor_end.logical_pos
+ && selection_end >= cursor_beg.logical_pos
+ {
+ // By default, we assume the entire line is selected.
+ let mut beg = 0;
+ let mut end = COORD_TYPE_SAFE_MAX;
+ let mut cursor = cursor_beg;
+
+ // 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);
+ 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);
+ end = cursor.visual_pos.x;
+ }
+
+ beg = beg.max(origin.x);
+ end = end.min(origin.x + text_width);
+
+ let left = destination.left + self.margin_width - origin.x;
+ let top = destination.top + y;
+ let rect = Rect { left: left + beg, top, right: left + end, bottom: top + 1 };
+
+ let mut bg = oklab_blend(
+ fb.indexed(IndexedColor::Foreground),
+ fb.indexed_alpha(IndexedColor::BrightBlue, 1, 2),
+ );
+ if !focused {
+ bg = oklab_blend(bg, fb.indexed_alpha(IndexedColor::Background, 1, 2))
+ };
+ let fg = fb.contrasted(bg);
+ fb.blend_bg(rect, bg);
+ fb.blend_fg(rect, fg);
+ }
+
+ 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, 0x7f3f3f3f);
+ }
+
+ 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,
+ },
+ 0x50282828,
+ );
+ }
+ }
+ }
+
+ Some(RenderResult { visual_pos_x_max })
+ }
+
+ /// Inserts `text` at the current cursor position.
+ ///
+ /// If there's a current selection, it will be replaced.
+ /// The selection is cleared after the call.
+ pub fn write(&mut self, text: &[u8], raw: bool) {
+ if text.is_empty() {
+ return;
+ }
+
+ if let Some((beg, end)) = self.selection_range_internal(false) {
+ self.edit_begin(HistoryType::Write, beg);
+ self.edit_delete(end);
+ self.set_selection(None);
+ }
+ if self.active_edit_depth <= 0 {
+ self.edit_begin(HistoryType::Write, self.cursor);
+ }
+
+ let mut offset = 0;
+ let scratch = scratch_arena(None);
+ let mut newline_buffer = ArenaString::new_in(&scratch);
+
+ 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 - (self.cursor.column % self.tab_size);
+ 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(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 tab_size = self.tab_size as usize;
+ 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 = 0usize;
+
+ '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 += tab_size - (newline_indentation % tab_size);
+ } 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 / tab_size;
+ newline_buffer.push_repeat('\t', tab_count);
+ newline_indentation -= tab_count * 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(' ', newline_indentation);
+ }
+
+ 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;
+ }
+ }
+
+ 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) {
+ debug_assert!(delta == -1 || delta == 1);
+
+ let mut beg;
+ let mut end;
+
+ if let Some(r) = self.selection_range_internal(false) {
+ (beg, end) = r;
+ } else {
+ if (delta == -1 && self.cursor.offset == 0)
+ || (delta == 1 && 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 mut chars = 0;
+ let mut offset = cursor.offset;
+
+ 'outer: loop {
+ let chunk = self.read_forward(offset);
+ if chunk.is_empty() {
+ break;
+ }
+
+ for &c in chunk {
+ if c == b'\n' || c == b'\r' || (c != b' ' && c != b'\t') {
+ break 'outer;
+ }
+ chars += 1;
+ }
+
+ offset += chunk.len();
+ }
+
+ Point { x: chars, y: cursor.logical_pos.y }
+ }
+
+ /// Unindents the current selection or line.
+ ///
+ /// TODO: This function is ripe for some optimizations:
+ /// * Instead of replacing the entire selection,
+ /// it should unindent each line directly (as if multiple cursors had been used).
+ /// * The cursor movement at the end is rather costly, but at least without word wrap
+ /// it should be possible to calculate it directly from the removed amount.
+ pub fn unindent(&mut self) {
+ let mut selection_beg = self.cursor.logical_pos;
+ let mut selection_end = selection_beg;
+
+ if let Some(TextBufferSelection { beg, end }) = self.selection {
+ selection_beg = beg;
+ selection_end = end;
+ }
+
+ let [beg, end] = minmax(selection_beg, selection_end);
+ let beg = self.cursor_move_to_logical_internal(self.cursor, Point { x: 0, y: beg.y });
+ let end = self.cursor_move_to_logical_internal(beg, Point { x: CoordType::MAX, y: end.y });
+
+ let mut replacement = Vec::new();
+ self.buffer.extract_raw(beg.offset, end.offset, &mut replacement, 0);
+
+ let initial_len = replacement.len();
+ let mut offset = 0;
+ let mut y = beg.logical_pos.y;
+
+ loop {
+ let mut remove = 0;
+
+ if replacement[offset] == b'\t' {
+ remove = 1;
+ } else {
+ while remove < self.tab_size as usize
+ && offset + remove < replacement.len()
+ && replacement[offset + remove] == b' '
+ {
+ remove += 1;
+ }
+ }
+
+ if remove > 0 {
+ replacement.drain(offset..offset + remove);
+ }
+
+ if y == selection_beg.y {
+ selection_beg.x -= remove as CoordType;
+ }
+ if y == selection_end.y {
+ selection_end.x -= remove as CoordType;
+ }
+
+ (offset, y) = unicode::newlines_forward(&replacement, offset, y, y + 1);
+ if offset >= replacement.len() {
+ break;
+ }
+ }
+
+ if replacement.len() == initial_len {
+ // Nothing to do.
+ return;
+ }
+
+ self.edit_begin(HistoryType::Other, beg);
+ self.edit_delete(end);
+ self.edit_write(&replacement);
+ self.edit_end();
+
+ if let Some(TextBufferSelection { beg, end }) = &mut self.selection {
+ *beg = selection_beg;
+ *end = selection_end;
+ }
+
+ self.set_cursor_internal(self.cursor_move_to_logical_internal(self.cursor, selection_end));
+ }
+
+ /// Extracts the contents of the current selection.
+ /// May optionally delete it, if requested. This is meant to be used for Ctrl+X.
+ pub fn extract_selection(&mut self, delete: bool) -> Vec {
+ 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);
+ }
+
+ 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 }
+ }
+
+ /// 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(),
+ }));
+ }
+
+ 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 self.cursor.logical_pos < undo.cursor {
+ out_off = 0; // Prepend the deleted portion.
+ undo.cursor = self.cursor.logical_pos; // Note the start of the deleted portion.
+ }
+
+ // 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;
+ 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.search = None;
+
+ // Also takes care of clearing `cursor_for_rendering`.
+ self.reflow(false);
+ }
+
+ /// 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) {
+ // 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)
+ };
+
+ let Some(list) = from.cursor_back_mut().remove_current_as_list() else {
+ return;
+ };
+
+ to.cursor_back_mut().splice_after(list);
+ }
+
+ let change = {
+ let to = if undo { &self.redo_stack } else { &self.undo_stack };
+ to.back().unwrap()
+ };
+
+ // Move to the point where the modification took place.
+ let cursor = self.cursor_move_to_logical_internal(self.cursor, change.borrow().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
+ };
+
+ {
+ let buffer_generation = self.buffer.generation();
+ 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) = unicode::newlines_forward(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;
+ }
+ }
+
+ // Also takes care of clearing `cursor_for_rendering`.
+ self.reflow(false);
+ }
+
+ /// 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
+}
diff --git a/pkgs/edit/src/buffer/navigation.rs b/pkgs/edit/src/buffer/navigation.rs
new file mode 100644
index 0000000..a3c6aa8
--- /dev/null
+++ b/pkgs/edit/src/buffer/navigation.rs
@@ -0,0 +1,290 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use std::ops::Range;
+
+use crate::document::ReadableDocument;
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+enum CharClass {
+ Whitespace,
+ Newline,
+ Separator,
+ Word,
+}
+
+const fn construct_classifier(seperators: &[u8]) -> [CharClass; 256] {
+ let mut classifier = [CharClass::Word; 256];
+
+ classifier[b' ' as usize] = CharClass::Whitespace;
+ classifier[b'\t' as usize] = CharClass::Whitespace;
+ classifier[b'\n' as usize] = CharClass::Newline;
+ classifier[b'\r' as usize] = CharClass::Newline;
+
+ let mut i = 0;
+ let len = seperators.len();
+ while i < len {
+ let ch = seperators[i];
+ assert!(ch < 128, "Only ASCII separators are supported.");
+ classifier[ch as usize] = CharClass::Separator;
+ i += 1;
+ }
+
+ classifier
+}
+
+const WORD_CLASSIFIER: [CharClass; 256] =
+ construct_classifier(br#"`~!@#$%^&*()-=+[{]}\|;:'",.<>/?"#);
+
+/// Finds the next word boundary given a document cursor offset.
+/// Returns the offset of the next word boundary.
+pub fn word_forward(doc: &dyn ReadableDocument, offset: usize) -> usize {
+ word_navigation(WordForward { doc, offset, chunk: &[], chunk_off: 0 })
+}
+
+/// The backward version of `word_forward`.
+pub fn word_backward(doc: &dyn ReadableDocument, offset: usize) -> usize {
+ word_navigation(WordBackward { doc, offset, chunk: &[], chunk_off: 0 })
+}
+
+/// Word navigation implementation. Matches the behavior of VS Code.
+fn word_navigation(mut nav: T) -> usize {
+ // First, fill `self.chunk` with at least 1 grapheme.
+ nav.read();
+
+ // Skip one newline, if any.
+ nav.skip_newline();
+
+ // Skip any whitespace.
+ nav.skip_class(CharClass::Whitespace);
+
+ // Skip one word or seperator and take note of the class.
+ let class = nav.peek(CharClass::Whitespace);
+ if matches!(class, CharClass::Separator | CharClass::Word) {
+ nav.next();
+
+ let off = nav.offset();
+
+ // Continue skipping the same class.
+ nav.skip_class(class);
+
+ // If the class was a separator and we only moved one character,
+ // continue skipping characters of the word class.
+ if off == nav.offset() && class == CharClass::Separator {
+ nav.skip_class(CharClass::Word);
+ }
+ }
+
+ nav.offset()
+}
+
+trait WordNavigation {
+ fn read(&mut self);
+ fn skip_newline(&mut self);
+ fn skip_class(&mut self, class: CharClass);
+ fn peek(&self, default: CharClass) -> CharClass;
+ fn next(&mut self);
+ fn offset(&self) -> usize;
+}
+
+struct WordForward<'a> {
+ doc: &'a dyn ReadableDocument,
+ offset: usize,
+ chunk: &'a [u8],
+ chunk_off: usize,
+}
+
+impl WordNavigation for WordForward<'_> {
+ fn read(&mut self) {
+ self.chunk = self.doc.read_forward(self.offset);
+ self.chunk_off = 0;
+ }
+
+ fn skip_newline(&mut self) {
+ // We can rely on the fact that the document does not split graphemes across chunks.
+ // = If there's a newline it's wholly contained in this chunk.
+ // Unlike with `WordBackward`, we can't check for CR and LF separately as only a CR followed
+ // by a LF is a newline. A lone CR in the document is just a regular control character.
+ self.chunk_off += match self.chunk.get(self.chunk_off) {
+ Some(&b'\n') => 1,
+ Some(&b'\r') if self.chunk.get(self.chunk_off + 1) == Some(&b'\n') => 2,
+ _ => 0,
+ }
+ }
+
+ fn skip_class(&mut self, class: CharClass) {
+ while !self.chunk.is_empty() {
+ while self.chunk_off < self.chunk.len() {
+ if WORD_CLASSIFIER[self.chunk[self.chunk_off] as usize] != class {
+ return;
+ }
+ self.chunk_off += 1;
+ }
+
+ self.offset += self.chunk.len();
+ self.chunk = self.doc.read_forward(self.offset);
+ self.chunk_off = 0;
+ }
+ }
+
+ fn peek(&self, default: CharClass) -> CharClass {
+ if self.chunk_off < self.chunk.len() {
+ WORD_CLASSIFIER[self.chunk[self.chunk_off] as usize]
+ } else {
+ default
+ }
+ }
+
+ fn next(&mut self) {
+ self.chunk_off += 1;
+ }
+
+ fn offset(&self) -> usize {
+ self.offset + self.chunk_off
+ }
+}
+
+struct WordBackward<'a> {
+ doc: &'a dyn ReadableDocument,
+ offset: usize,
+ chunk: &'a [u8],
+ chunk_off: usize,
+}
+
+impl WordNavigation for WordBackward<'_> {
+ fn read(&mut self) {
+ self.chunk = self.doc.read_backward(self.offset);
+ self.chunk_off = self.chunk.len();
+ }
+
+ fn skip_newline(&mut self) {
+ // We can rely on the fact that the document does not split graphemes across chunks.
+ // = If there's a newline it's wholly contained in this chunk.
+ if self.chunk_off > 0 && self.chunk[self.chunk_off - 1] == b'\n' {
+ self.chunk_off -= 1;
+ }
+ if self.chunk_off > 0 && self.chunk[self.chunk_off - 1] == b'\r' {
+ self.chunk_off -= 1;
+ }
+ }
+
+ fn skip_class(&mut self, class: CharClass) {
+ while !self.chunk.is_empty() {
+ while self.chunk_off > 0 {
+ if WORD_CLASSIFIER[self.chunk[self.chunk_off - 1] as usize] != class {
+ return;
+ }
+ self.chunk_off -= 1;
+ }
+
+ self.offset -= self.chunk.len();
+ self.chunk = self.doc.read_backward(self.offset);
+ self.chunk_off = self.chunk.len();
+ }
+ }
+
+ fn peek(&self, default: CharClass) -> CharClass {
+ if self.chunk_off > 0 {
+ WORD_CLASSIFIER[self.chunk[self.chunk_off - 1] as usize]
+ } else {
+ default
+ }
+ }
+
+ fn next(&mut self) {
+ self.chunk_off -= 1;
+ }
+
+ fn offset(&self) -> usize {
+ self.offset - self.chunk.len() + self.chunk_off
+ }
+}
+
+/// Returns the offset range of the "word" at the given offset.
+/// Does not cross newlines. Works similar to VS Code.
+pub fn word_select(doc: &dyn ReadableDocument, offset: usize) -> Range {
+ 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);
+ }
+}
diff --git a/pkgs/edit/src/cell.rs b/pkgs/edit/src/cell.rs
new file mode 100644
index 0000000..0e29039
--- /dev/null
+++ b/pkgs/edit/src/cell.rs
@@ -0,0 +1,84 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! [`std::cell::RefCell`], but without runtime checks in release builds.
+
+#[cfg(debug_assertions)]
+pub use debug::*;
+#[cfg(not(debug_assertions))]
+pub use release::*;
+
+#[allow(unused)]
+#[cfg(debug_assertions)]
+mod debug {
+ pub type SemiRefCell = 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: &Ref<'b, T>) -> Ref<'b, T> {
+ Ref(orig.0)
+ }
+ }
+
+ impl<'b, T> std::ops::Deref for Ref<'b, T> {
+ type Target = T;
+
+ #[inline(always)]
+ fn deref(&self) -> &Self::Target {
+ self.0
+ }
+ }
+
+ #[repr(transparent)]
+ pub struct RefMut<'b, T>(&'b mut T);
+
+ impl<'b, T> std::ops::Deref for RefMut<'b, T> {
+ type Target = T;
+
+ #[inline(always)]
+ fn deref(&self) -> &Self::Target {
+ self.0
+ }
+ }
+
+ impl<'b, T> std::ops::DerefMut for RefMut<'b, T> {
+ #[inline(always)]
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ self.0
+ }
+ }
+}
diff --git a/pkgs/edit/src/document.rs b/pkgs/edit/src/document.rs
new file mode 100644
index 0000000..0ae9eb8
--- /dev/null
+++ b/pkgs/edit/src/document.rs
@@ -0,0 +1,109 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! Abstractions over reading/writing arbitrary text containers.
+
+use std::ffi::OsString;
+use std::mem;
+use std::ops::Range;
+use std::path::PathBuf;
+
+use crate::arena::{ArenaString, scratch_arena};
+use crate::helpers::ReplaceRange as _;
+
+/// An abstraction over reading from text containers.
+pub trait ReadableDocument {
+ /// Read some bytes starting at (including) the given absolute offset.
+ ///
+ /// # Warning
+ ///
+ /// * Be lenient on inputs:
+ /// * The given offset may be out of bounds and you MUST clamp it.
+ /// * You should not assume that offsets are at grapheme cluster boundaries.
+ /// * Be strict on outputs:
+ /// * You MUST NOT break grapheme clusters across chunks.
+ /// * You MUST NOT return an empty slice unless the offset is at or beyond the end.
+ fn read_forward(&self, off: usize) -> &[u8];
+
+ /// Read some bytes before (but not including) the given absolute offset.
+ ///
+ /// # Warning
+ ///
+ /// * Be lenient on inputs:
+ /// * The given offset may be out of bounds and you MUST clamp it.
+ /// * You should not assume that offsets are at grapheme cluster boundaries.
+ /// * Be strict on outputs:
+ /// * You MUST NOT break grapheme clusters across chunks.
+ /// * You MUST NOT return an empty slice unless the offset is zero.
+ fn read_backward(&self, off: usize) -> &[u8];
+}
+
+/// An abstraction over writing to text containers.
+pub trait WriteableDocument: ReadableDocument {
+ /// Replace the given range with the given bytes.
+ ///
+ /// # Warning
+ ///
+ /// * The given range may be out of bounds and you MUST clamp it.
+ /// * The replacement may not be valid UTF8.
+ fn replace(&mut self, range: Range, 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 scratch = scratch_arena(None);
+ let utf8 = ArenaString::from_utf8_lossy(&scratch, replacement);
+ let src = match &utf8 {
+ Ok(s) => s,
+ Err(s) => s.as_str(),
+ };
+
+ // SAFETY: `range` is guaranteed to be on codepoint boundaries.
+ unsafe { self.as_mut_vec() }.replace_range(range, src.as_bytes());
+ }
+}
+
+impl ReadableDocument for PathBuf {
+ fn read_forward(&self, off: usize) -> &[u8] {
+ let s = self.as_os_str().as_encoded_bytes();
+ &s[off.min(s.len())..]
+ }
+
+ fn read_backward(&self, off: usize) -> &[u8] {
+ let s = self.as_os_str().as_encoded_bytes();
+ &s[..off.min(s.len())]
+ }
+}
+
+impl WriteableDocument for PathBuf {
+ fn replace(&mut self, range: Range, replacement: &[u8]) {
+ let mut vec = mem::take(self).into_os_string().into_encoded_bytes();
+ vec.replace_range(range, replacement);
+ *self = unsafe { PathBuf::from(OsString::from_encoded_bytes_unchecked(vec)) };
+ }
+}
diff --git a/pkgs/edit/src/framebuffer.rs b/pkgs/edit/src/framebuffer.rs
new file mode 100644
index 0000000..71031b3
--- /dev/null
+++ b/pkgs/edit/src/framebuffer.rs
@@ -0,0 +1,888 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! A shoddy framebuffer for terminal applications.
+
+use std::cell::Cell;
+use std::fmt::Write;
+use std::ops::{BitOr, BitXor};
+use std::ptr;
+use std::slice::ChunksExact;
+
+use crate::arena::{Arena, ArenaString};
+use crate::helpers::{CoordType, Point, Rect, Size};
+use crate::oklab::{oklab_blend, srgb_to_oklab};
+use crate::simd::{MemsetSafe, memset};
+use crate::unicode::MeasurementConfig;
+
+// Same constants as used in the PCG family of RNGs.
+#[cfg(target_pointer_width = "32")]
+const HASH_MULTIPLIER: usize = 747796405; // https://doi.org/10.1090/S0025-5718-99-00996-5, Table 5
+#[cfg(target_pointer_width = "64")]
+const HASH_MULTIPLIER: usize = 6364136223846793005; // Knuth's MMIX multiplier
+
+/// The size of our cache table. 1<<8 = 256.
+const CACHE_TABLE_LOG2_SIZE: usize = 8;
+const CACHE_TABLE_SIZE: usize = 1 << CACHE_TABLE_LOG2_SIZE;
+/// To index into the cache table, we use `color * HASH_MULTIPLIER` as the hash.
+/// Since the multiplication "shifts" the bits up, we don't just mask the lowest
+/// 8 bits out, but rather shift 56 bits down to get the best bits from the top.
+const CACHE_TABLE_SHIFT: usize = usize::BITS as usize - CACHE_TABLE_LOG2_SIZE;
+
+/// Standard 16 VT & default foreground/background colors.
+#[derive(Clone, Copy)]
+pub enum IndexedColor {
+ Black,
+ Red,
+ Green,
+ Yellow,
+ Blue,
+ Magenta,
+ Cyan,
+ White,
+ BrightBlack,
+ BrightRed,
+ BrightGreen,
+ BrightYellow,
+ BrightBlue,
+ BrightMagenta,
+ BrightCyan,
+ BrightWhite,
+
+ Background,
+ Foreground,
+}
+
+/// Number of indices used by [`IndexedColor`].
+pub const INDEXED_COLORS_COUNT: usize = 18;
+
+/// Fallback theme. Matches Windows Terminal's Ottosson theme.
+pub const DEFAULT_THEME: [u32; INDEXED_COLORS_COUNT] = [
+ 0xff000000, 0xff212cbe, 0xff3aae3f, 0xff4a9abe, 0xffbe4d20, 0xffbe54bb, 0xffb2a700, 0xffbebebe,
+ 0xff808080, 0xff303eff, 0xff51ea58, 0xff44c9ff, 0xffff6a2f, 0xffff74fc, 0xfff0e100, 0xffffffff,
+ 0xff000000, 0xffffffff,
+];
+
+/// A shoddy framebuffer for terminal applications.
+///
+/// The idea is that you create a [`Framebuffer`], draw a bunch of text and
+/// colors into it, and it takes care of figuring out what changed since the
+/// last rendering and sending the differences as VT to the terminal.
+///
+/// This is an improvement over how many other terminal applications work,
+/// as they fail to accurately track what changed. If you watch the output
+/// of `vim` for instance, you'll notice that it redraws unrelated parts of
+/// the screen all the time.
+pub struct Framebuffer {
+ /// Store the color palette.
+ indexed_colors: [u32; INDEXED_COLORS_COUNT],
+ /// Front and back buffers. Indexed by `frame_counter & 1`.
+ buffers: [Buffer; 2],
+ /// The current frame counter. Increments on every `flip` call.
+ frame_counter: usize,
+ /// The colors used for `contrast()`. It stores the default colors
+ /// of the palette as [dark, light], unless the palette is recognized
+ /// as a light them, in which case it swaps them.
+ auto_colors: [u32; 2],
+ /// A cache table for previously contrasted colors.
+ /// See:
+ contrast_colors: [Cell<(u32, u32)>; CACHE_TABLE_SIZE],
+ background_fill: u32,
+ foreground_fill: u32,
+}
+
+impl Framebuffer {
+ /// Creates a new framebuffer.
+ pub fn new() -> Self {
+ Self {
+ indexed_colors: DEFAULT_THEME,
+ buffers: Default::default(),
+ frame_counter: 0,
+ auto_colors: [
+ DEFAULT_THEME[IndexedColor::Black as usize],
+ DEFAULT_THEME[IndexedColor::White as usize],
+ ],
+ contrast_colors: [const { Cell::new((0, 0)) }; CACHE_TABLE_SIZE],
+ background_fill: DEFAULT_THEME[IndexedColor::Background as usize],
+ foreground_fill: DEFAULT_THEME[IndexedColor::Foreground as usize],
+ }
+ }
+
+ /// Sets the base color palette.
+ ///
+ /// If you call this method, [`Framebuffer`] expects that you
+ /// successfully detect the light/dark mode of the terminal.
+ pub fn set_indexed_colors(&mut self, colors: [u32; INDEXED_COLORS_COUNT]) {
+ self.indexed_colors = colors;
+ self.background_fill = 0;
+ self.foreground_fill = 0;
+
+ self.auto_colors = [
+ self.indexed_colors[IndexedColor::Black as usize],
+ self.indexed_colors[IndexedColor::BrightWhite as usize],
+ ];
+ if !Self::is_dark(self.auto_colors[0]) {
+ self.auto_colors.swap(0, 1);
+ }
+ }
+
+ /// Begins a new frame with the given `size`.
+ pub fn flip(&mut self, size: Size) {
+ if size != self.buffers[0].bg_bitmap.size {
+ for buffer in &mut self.buffers {
+ buffer.text = LineBuffer::new(size);
+ buffer.bg_bitmap = Bitmap::new(size);
+ buffer.fg_bitmap = Bitmap::new(size);
+ buffer.attributes = AttributeBuffer::new(size);
+ }
+
+ let front = &mut self.buffers[self.frame_counter & 1];
+ // Trigger a full redraw. (Yes, it's a hack.)
+ front.fg_bitmap.fill(1);
+ // Trigger a cursor update as well, just to be sure.
+ front.cursor = Cursor::new_invalid();
+ }
+
+ self.frame_counter = self.frame_counter.wrapping_add(1);
+
+ let back = &mut self.buffers[self.frame_counter & 1];
+
+ back.text.fill_whitespace();
+ back.bg_bitmap.fill(self.background_fill);
+ back.fg_bitmap.fill(self.foreground_fill);
+ back.attributes.reset();
+ back.cursor = Cursor::new_disabled();
+ }
+
+ /// Replaces text contents in a single line of the framebuffer.
+ /// All coordinates are in viewport coordinates.
+ /// Assumes that control characters have been replaced or escaped.
+ pub fn replace_text(
+ &mut self,
+ y: CoordType,
+ origin_x: CoordType,
+ clip_right: CoordType,
+ text: &str,
+ ) {
+ let back = &mut self.buffers[self.frame_counter & 1];
+ back.text.replace_text(y, origin_x, clip_right, text)
+ }
+
+ /// Draws a scrollbar in the given `track` rectangle.
+ ///
+ /// Not entirely sure why I put it here instead of elsewhere.
+ ///
+ /// # Parameters
+ ///
+ /// * `clip_rect`: Clips the rendering to this rectangle.
+ /// This is relevant when you have scrollareas inside scrollareas.
+ /// * `track`: The rectangle in which to draw the scrollbar.
+ /// In absolute viewport coordinates.
+ /// * `content_offset`: The current offset of the scrollarea.
+ /// * `content_height`: The height of the scrollarea content.
+ pub fn draw_scrollbar(
+ &mut self,
+ clip_rect: Rect,
+ track: Rect,
+ content_offset: CoordType,
+ content_height: CoordType,
+ ) -> CoordType {
+ let track_clipped = track.intersect(clip_rect);
+ if track_clipped.is_empty() {
+ return 0;
+ }
+
+ let viewport_height = track.height();
+ // The content height is at least the viewport height.
+ let content_height = content_height.max(viewport_height);
+
+ // No need to draw a scrollbar if the content fits in the viewport.
+ let content_offset_max = content_height - viewport_height;
+ if content_offset_max == 0 {
+ return 0;
+ }
+
+ // The content offset must be at least one viewport height from the bottom.
+ // You don't want to scroll past the end after all...
+ let content_offset = content_offset.clamp(0, content_offset_max);
+
+ // In order to increase the visual resolution of the scrollbar,
+ // we'll use 1/8th blocks to represent the thumb.
+ // First, scale the offsets to get that 1/8th resolution.
+ let viewport_height = viewport_height as i64 * 8;
+ let content_offset_max = content_offset_max as i64 * 8;
+ let content_offset = content_offset as i64 * 8;
+ let content_height = content_height as i64 * 8;
+
+ // The proportional thumb height (0-1) is the fraction of viewport and
+ // content height. The taller the content, the smaller the thumb:
+ // = viewport_height / content_height
+ // We then scale that to the viewport height to get the height in 1/8th units.
+ // = viewport_height * viewport_height / content_height
+ // We add content_height/2 to round the integer division, which results in a numerator of:
+ // = viewport_height * viewport_height + content_height / 2
+ let numerator = viewport_height * viewport_height + content_height / 2;
+ let thumb_height = numerator / content_height;
+ // Ensure the thumb has a minimum size of 1 row.
+ let thumb_height = thumb_height.max(8);
+
+ // The proportional thumb top position (0-1) is:
+ // = content_offset / content_offset_max
+ // The maximum thumb top position is the viewport height minus the thumb height:
+ // = viewport_height - thumb_height
+ // To get the thumb top position in 1/8th units, we multiply both:
+ // = (viewport_height - thumb_height) * content_offset / content_offset_max
+ // We add content_offset_max/2 to round the integer division, which results in a numerator of:
+ // = (viewport_height - thumb_height) * content_offset + content_offset_max / 2
+ let numerator = (viewport_height - thumb_height) * content_offset + content_offset_max / 2;
+ let thumb_top = numerator / content_offset_max;
+ // The thumb bottom position is the thumb top position plus the thumb height.
+ let thumb_bottom = thumb_top + thumb_height;
+
+ // Shift to absolute coordinates.
+ let thumb_top = thumb_top + track.top as i64 * 8;
+ let thumb_bottom = thumb_bottom + track.top as i64 * 8;
+
+ // Clamp to the visible area.
+ let thumb_top = thumb_top.max(track_clipped.top as i64 * 8);
+ let thumb_bottom = thumb_bottom.min(track_clipped.bottom as i64 * 8);
+
+ // Calculate the height of the top/bottom cell of the thumb.
+ let top_fract = (thumb_top % 8) as CoordType;
+ let bottom_fract = (thumb_bottom % 8) as CoordType;
+
+ // Shift to absolute coordinates.
+ let thumb_top = ((thumb_top + 7) / 8) as CoordType;
+ let thumb_bottom = (thumb_bottom / 8) as CoordType;
+
+ self.blend_bg(track_clipped, self.indexed(IndexedColor::BrightBlack));
+ self.blend_fg(track_clipped, self.indexed(IndexedColor::BrightWhite));
+
+ // Draw the full blocks.
+ for y in thumb_top..thumb_bottom {
+ self.replace_text(y, track_clipped.left, track_clipped.right, "█");
+ }
+
+ // Draw the top/bottom cell of the thumb.
+ // U+2581 to U+2588, 1/8th block to 8/8th block elements glyphs: ▁▂▃▄▅▆▇█
+ // In UTF8: E2 96 81 to E2 96 88
+ let mut fract_buf = [0xE2, 0x96, 0x88];
+ if top_fract != 0 {
+ fract_buf[2] = (0x88 - top_fract) as u8;
+ self.replace_text(thumb_top - 1, track_clipped.left, track_clipped.right, unsafe {
+ std::str::from_utf8_unchecked(&fract_buf)
+ });
+ }
+ if bottom_fract != 0 {
+ fract_buf[2] = (0x88 - bottom_fract) as u8;
+ self.replace_text(thumb_bottom, track_clipped.left, track_clipped.right, unsafe {
+ std::str::from_utf8_unchecked(&fract_buf)
+ });
+ let rect = Rect {
+ left: track_clipped.left,
+ top: thumb_bottom,
+ right: track_clipped.right,
+ bottom: thumb_bottom + 1,
+ };
+ self.blend_bg(rect, self.indexed(IndexedColor::BrightWhite));
+ self.blend_fg(rect, self.indexed(IndexedColor::BrightBlack));
+ }
+
+ ((thumb_height + 4) / 8) as CoordType
+ }
+
+ #[inline]
+ pub fn indexed(&self, index: IndexedColor) -> u32 {
+ self.indexed_colors[index as usize]
+ }
+
+ /// Returns a color from the palette.
+ ///
+ /// To facilitate constant folding by the compiler,
+ /// alpha is given as a fraction (`numerator` / `denominator`).
+ #[inline]
+ pub fn indexed_alpha(&self, index: IndexedColor, numerator: u32, denominator: u32) -> u32 {
+ let c = self.indexed_colors[index as usize];
+ let a = 255 * numerator / denominator;
+ let r = (((c >> 16) & 0xFF) * numerator) / denominator;
+ let g = (((c >> 8) & 0xFF) * numerator) / denominator;
+ let b = ((c & 0xFF) * numerator) / denominator;
+ a << 24 | r << 16 | g << 8 | b
+ }
+
+ /// Returns a color opposite to the brightness of the given `color`.
+ pub fn contrasted(&self, color: u32) -> u32 {
+ let idx = (color as usize).wrapping_mul(HASH_MULTIPLIER) >> CACHE_TABLE_SHIFT;
+ let slot = self.contrast_colors[idx].get();
+ if slot.0 == color { slot.1 } else { self.contrasted_slow(color) }
+ }
+
+ #[cold]
+ fn contrasted_slow(&self, color: u32) -> u32 {
+ let idx = (color as usize).wrapping_mul(HASH_MULTIPLIER) >> CACHE_TABLE_SHIFT;
+ let contrast = self.auto_colors[Self::is_dark(color) as usize];
+ self.contrast_colors[idx].set((color, contrast));
+ contrast
+ }
+
+ fn is_dark(color: u32) -> bool {
+ srgb_to_oklab(color).l < 0.5
+ }
+
+ /// Blends the given sRGB color onto the background bitmap.
+ ///
+ /// TODO: The current approach blends foreground/background independently,
+ /// but ideally `blend_bg` with semi-transparent dark should also darken text below it.
+ pub fn blend_bg(&mut self, target: Rect, bg: u32) {
+ let back = &mut self.buffers[self.frame_counter & 1];
+ back.bg_bitmap.blend(target, bg);
+ }
+
+ /// Blends the given sRGB color onto the foreground bitmap.
+ ///
+ /// TODO: The current approach blends foreground/background independently,
+ /// but ideally `blend_fg` should blend with the background color below it.
+ pub fn blend_fg(&mut self, target: Rect, fg: u32) {
+ let back = &mut self.buffers[self.frame_counter & 1];
+ back.fg_bitmap.blend(target, fg);
+ }
+
+ /// Reverses the foreground and background colors in the given rectangle.
+ pub fn reverse(&mut self, target: Rect) {
+ let back = &mut self.buffers[self.frame_counter & 1];
+
+ let target = target.intersect(back.bg_bitmap.size.as_rect());
+ if target.is_empty() {
+ return;
+ }
+
+ let top = target.top as usize;
+ let bottom = target.bottom as usize;
+ let left = target.left as usize;
+ let right = target.right as usize;
+ let stride = back.bg_bitmap.size.width as usize;
+
+ for y in top..bottom {
+ let beg = y * stride + left;
+ let end = y * stride + right;
+ let bg = &mut back.bg_bitmap.data[beg..end];
+ let fg = &mut back.fg_bitmap.data[beg..end];
+ bg.swap_with_slice(fg);
+ }
+ }
+
+ /// Replaces VT attributes in the given rectangle.
+ pub fn replace_attr(&mut self, target: Rect, mask: Attributes, attr: Attributes) {
+ let back = &mut self.buffers[self.frame_counter & 1];
+ back.attributes.replace(target, mask, attr);
+ }
+
+ /// Sets the current visible cursor position and type.
+ ///
+ /// Call this when focus is inside an editable area and you want to show the cursor.
+ pub fn set_cursor(&mut self, pos: Point, overtype: bool) {
+ let back = &mut self.buffers[self.frame_counter & 1];
+ back.cursor.pos = pos;
+ back.cursor.overtype = overtype;
+ }
+
+ /// Renders the framebuffer contents accumulated since the
+ /// last call to `flip()` and returns them serialized as VT.
+ pub fn render<'a>(&mut self, arena: &'a Arena) -> ArenaString<'a> {
+ let idx = self.frame_counter & 1;
+ // Borrows the front/back buffers without letting Rust know that we have a reference to self.
+ // SAFETY: Well this is certainly correct, but whether Rust and its strict rules likes it is another question.
+ let (back, front) = unsafe {
+ let ptr = self.buffers.as_mut_ptr();
+ let back = &mut *ptr.add(idx);
+ let front = &*ptr.add(1 - idx);
+ (back, front)
+ };
+
+ let mut front_lines = front.text.lines.iter(); // hahaha
+ let mut front_bgs = front.bg_bitmap.iter();
+ let mut front_fgs = front.fg_bitmap.iter();
+ let mut front_attrs = front.attributes.iter();
+
+ let mut back_lines = back.text.lines.iter();
+ let mut back_bgs = back.bg_bitmap.iter();
+ let mut back_fgs = back.fg_bitmap.iter();
+ let mut back_attrs = back.attributes.iter();
+
+ let mut result = ArenaString::new_in(arena);
+ let mut last_bg = u64::MAX;
+ let mut last_fg = u64::MAX;
+ let mut last_attr = Attributes::None;
+
+ for y in 0..front.text.size.height {
+ // SAFETY: The only thing that changes the size of these containers,
+ // is the reset() method and it always resets front/back to the same size.
+ let front_line = unsafe { front_lines.next().unwrap_unchecked() };
+ let front_bg = unsafe { front_bgs.next().unwrap_unchecked() };
+ let front_fg = unsafe { front_fgs.next().unwrap_unchecked() };
+ let front_attr = unsafe { front_attrs.next().unwrap_unchecked() };
+
+ let back_line = unsafe { back_lines.next().unwrap_unchecked() };
+ let back_bg = unsafe { back_bgs.next().unwrap_unchecked() };
+ let back_fg = unsafe { back_fgs.next().unwrap_unchecked() };
+ let back_attr = unsafe { back_attrs.next().unwrap_unchecked() };
+
+ // TODO: Ideally, we should properly diff the contents and so if
+ // only parts of a line change, we should only update those parts.
+ if front_line == back_line
+ && front_bg == back_bg
+ && front_fg == back_fg
+ && front_attr == back_attr
+ {
+ continue;
+ }
+
+ let line_bytes = back_line.as_bytes();
+ let mut cfg = MeasurementConfig::new(&line_bytes);
+ let mut chunk_end = 0;
+
+ if result.is_empty() {
+ result.push_str("\x1b[m");
+ }
+ _ = write!(result, "\x1b[{};1H", y + 1);
+
+ while {
+ let bg = back_bg[chunk_end];
+ let fg = back_fg[chunk_end];
+ let attr = back_attr[chunk_end];
+
+ // Chunk into runs of the same color.
+ while {
+ chunk_end += 1;
+ chunk_end < back_bg.len()
+ && back_bg[chunk_end] == bg
+ && back_fg[chunk_end] == fg
+ && back_attr[chunk_end] == attr
+ } {}
+
+ if last_bg != bg as u64 {
+ last_bg = bg as u64;
+ self.format_color(&mut result, false, bg);
+ }
+
+ if last_fg != fg as u64 {
+ last_fg = fg as u64;
+ self.format_color(&mut result, true, fg);
+ }
+
+ if last_attr != attr {
+ let diff = last_attr ^ attr;
+ if diff.is(Attributes::Italic) {
+ if attr.is(Attributes::Italic) {
+ result.push_str("\x1b[3m");
+ } else {
+ result.push_str("\x1b[23m");
+ }
+ }
+ if diff.is(Attributes::Underlined) {
+ if attr.is(Attributes::Underlined) {
+ result.push_str("\x1b[4m");
+ } else {
+ result.push_str("\x1b[24m");
+ }
+ }
+ last_attr = attr;
+ }
+
+ let beg = cfg.cursor().offset;
+ let end = cfg.goto_visual(Point { x: chunk_end as CoordType, y: 0 }).offset;
+ result.push_str(&back_line[beg..end]);
+
+ chunk_end < back_bg.len()
+ } {}
+ }
+
+ // If the cursor has changed since the last frame we naturally need to update it,
+ // but this also applies if the code above wrote to the screen,
+ // as it uses CUP sequences to reposition the cursor for writing.
+ if !result.is_empty() || back.cursor != front.cursor {
+ if back.cursor.pos.x >= 0 && back.cursor.pos.y >= 0 {
+ // CUP to the cursor position.
+ // DECSCUSR to set the cursor style.
+ // DECTCEM to show the cursor.
+ _ = write!(
+ result,
+ "\x1b[{};{}H\x1b[{} q\x1b[?25h",
+ back.cursor.pos.y + 1,
+ back.cursor.pos.x + 1,
+ if back.cursor.overtype { 1 } else { 5 }
+ );
+ } else {
+ // DECTCEM to hide the cursor.
+ result.push_str("\x1b[?25l");
+ }
+ }
+
+ result
+ }
+
+ fn format_color(&self, dst: &mut ArenaString, fg: bool, mut color: u32) {
+ let typ = if fg { '3' } else { '4' };
+
+ // Some terminals support transparent backgrounds which are used
+ // if the default background color is active (CSI 49 m).
+ //
+ // If [`Framebuffer::set_indexed_colors`] was never called, we assume
+ // that the terminal doesn't support transparency and initialize the
+ // background bitmap with the `DEFAULT_THEME` default background color.
+ // Otherwise, we assume that the terminal supports transparency
+ // and initialize it with 0x00000000 (transparent).
+ //
+ // We also apply this to the foreground color, because it compresses
+ // the output slightly and ensures that we keep "default foreground"
+ // and "color that happens to be default foreground" separate.
+ // (This also applies to the background color by the way.)
+ if color == 0 {
+ _ = write!(dst, "\x1b[{typ}9m");
+ return;
+ }
+
+ if (color & 0xff000000) != 0xff000000 {
+ let idx = if fg { IndexedColor::Foreground } else { IndexedColor::Background };
+ let dst = self.indexed(idx);
+ color = oklab_blend(dst, color);
+ }
+
+ let r = color & 0xff;
+ let g = (color >> 8) & 0xff;
+ let b = (color >> 16) & 0xff;
+ _ = write!(dst, "\x1b[{typ}8;2;{r};{g};{b}m");
+ }
+}
+
+#[derive(Default)]
+struct Buffer {
+ text: LineBuffer,
+ bg_bitmap: Bitmap,
+ fg_bitmap: Bitmap,
+ attributes: AttributeBuffer,
+ cursor: Cursor,
+}
+
+/// A buffer for the text contents of the framebuffer.
+#[derive(Default)]
+struct LineBuffer {
+ lines: Vec,
+ size: Size,
+}
+
+impl LineBuffer {
+ fn new(size: Size) -> Self {
+ Self { lines: vec![String::new(); size.height as usize], size }
+ }
+
+ fn fill_whitespace(&mut self) {
+ let width = self.size.width as usize;
+ for l in &mut self.lines {
+ l.clear();
+ l.reserve(width + width / 2);
+
+ let buf = unsafe { l.as_mut_vec() };
+ // Compiles down to `memset()`.
+ buf.extend(std::iter::repeat_n(b' ', width));
+ }
+ }
+
+ /// Replaces text contents in a single line of the framebuffer.
+ /// All coordinates are in viewport coordinates.
+ /// Assumes that control characters have been replaced or escaped.
+ fn replace_text(
+ &mut self,
+ y: CoordType,
+ origin_x: CoordType,
+ clip_right: CoordType,
+ text: &str,
+ ) {
+ let Some(line) = self.lines.get_mut(y as usize) else {
+ return;
+ };
+
+ let bytes = text.as_bytes();
+ let clip_right = clip_right.clamp(0, self.size.width);
+ let layout_width = clip_right - origin_x;
+
+ // Can't insert text that can't fit or is empty.
+ if layout_width <= 0 || bytes.is_empty() {
+ return;
+ }
+
+ let mut cfg = MeasurementConfig::new(&bytes);
+
+ // Check if the text intersects with the left edge of the framebuffer
+ // and figure out the parts that are inside.
+ let mut left = origin_x;
+ if left < 0 {
+ let mut cursor = cfg.goto_visual(Point { x: -left, y: 0 });
+
+ if left + cursor.visual_pos.x < 0 && cursor.offset < text.len() {
+ // `-left` must've intersected a wide glyph and since goto_visual stops _before_ reaching the target,
+ // we stoped before the wide glyph and thus must step forward to the next glyph.
+ cursor = cfg.goto_logical(Point { x: cursor.logical_pos.x + 1, y: 0 });
+ }
+
+ left += cursor.visual_pos.x;
+ }
+
+ // If the text still starts outside the framebuffer, we must've ran out of text above.
+ // Otherwise, if it starts outside the right edge to begin with, we can't insert it anyway.
+ if left < 0 || left >= clip_right {
+ return;
+ }
+
+ // Measure the width of the new text (= `res_new.visual_target.x`).
+ let beg_off = cfg.cursor().offset;
+ let end = cfg.goto_visual(Point { x: layout_width, y: 0 });
+
+ // Figure out at which byte offset the new text gets inserted.
+ let right = left + end.visual_pos.x;
+ let line_bytes = line.as_bytes();
+ let mut cfg_old = MeasurementConfig::new(&line_bytes);
+ let res_old_beg = cfg_old.goto_visual(Point { x: left, y: 0 });
+ let mut res_old_end = cfg_old.goto_visual(Point { x: right, y: 0 });
+
+ // Since the goto functions will always stop short of the target position,
+ // we need to manually step beyond it if we intersect with a wide glyph.
+ if res_old_end.visual_pos.x < right {
+ res_old_end = cfg_old.goto_logical(Point { x: res_old_end.logical_pos.x + 1, y: 0 });
+ }
+
+ // If we intersect a wide glyph, we need to pad the new text with spaces.
+ let src = &text[beg_off..end.offset];
+ let overlap_beg = (left - res_old_beg.visual_pos.x).max(0) as usize;
+ let overlap_end = (res_old_end.visual_pos.x - right).max(0) as usize;
+ let total_add = src.len() + overlap_beg + overlap_end;
+ let total_del = res_old_end.offset - res_old_beg.offset;
+
+ // This is basically a hand-written version of `Vec::splice()`,
+ // but for strings under the assumption that all inputs are valid.
+ // It also takes care of `overlap_beg` and `overlap_end` by inserting spaces.
+ unsafe {
+ // SAFETY: Our ucd code only returns valid UTF-8 offsets.
+ // If it didn't that'd be a priority -9000 bug for any text editor.
+ // And apart from that, all inputs are &str (= UTF8).
+ let dst = line.as_mut_vec();
+
+ let dst_len = dst.len();
+ let src_len = src.len();
+
+ // Make room for the new elements. NOTE that this must be done before
+ // we call as_mut_ptr, or else we risk accessing a stale pointer.
+ // We only need to reserve as much as the string actually grows by.
+ dst.reserve(total_add.saturating_sub(total_del));
+
+ // Move the pointer to the start of the insert.
+ let mut ptr = dst.as_mut_ptr().add(res_old_beg.offset);
+
+ // Move the tail end of the string by `total_add - total_del`-many bytes.
+ // This both effectively deletes the old text and makes room for the new text.
+ if total_add != total_del {
+ // Move the tail of the vector to make room for the new elements.
+ ptr::copy(
+ ptr.add(total_del),
+ ptr.add(total_add),
+ dst_len - total_del - res_old_beg.offset,
+ );
+ }
+
+ // Pad left.
+ for _ in 0..overlap_beg {
+ ptr.write(b' ');
+ ptr = ptr.add(1);
+ }
+
+ // Copy the new elements into the vector.
+ ptr::copy_nonoverlapping(src.as_ptr(), ptr, src_len);
+ ptr = ptr.add(src_len);
+
+ // Pad right.
+ for _ in 0..overlap_end {
+ ptr.write(b' ');
+ ptr = ptr.add(1);
+ }
+
+ // Update the length of the vector.
+ dst.set_len(dst_len - total_del + total_add);
+ }
+ }
+}
+
+/// An sRGB bitmap.
+#[derive(Default)]
+struct Bitmap {
+ data: Vec,
+ size: Size,
+}
+
+impl Bitmap {
+ fn new(size: Size) -> Self {
+ Self { data: vec![0; (size.width * size.height) as usize], size }
+ }
+
+ fn fill(&mut self, color: u32) {
+ memset(&mut self.data, color);
+ }
+
+ /// Blends the given sRGB color onto the bitmap.
+ ///
+ /// This uses the `oklab` color space for blending so the
+ /// resulting colors may look different from what you'd expect.
+ fn blend(&mut self, target: Rect, color: u32) {
+ if (color & 0xff000000) == 0x00000000 {
+ return;
+ }
+
+ let target = target.intersect(self.size.as_rect());
+ if target.is_empty() {
+ return;
+ }
+
+ let top = target.top as usize;
+ let bottom = target.bottom as usize;
+ let left = target.left as usize;
+ let right = target.right as usize;
+ let stride = self.size.width as usize;
+
+ for y in top..bottom {
+ let beg = y * stride + left;
+ let end = y * stride + right;
+ let data = &mut self.data[beg..end];
+
+ if (color & 0xff000000) == 0xff000000 {
+ memset(data, color);
+ } else {
+ let end = data.len();
+ let mut off = 0;
+
+ while {
+ let c = data[off];
+
+ // Chunk into runs of the same color, so that we only call alpha_blend once per run.
+ let chunk_beg = off;
+ while {
+ off += 1;
+ off < end && data[off] == c
+ } {}
+ let chunk_end = off;
+
+ let c = oklab_blend(c, color);
+ memset(&mut data[chunk_beg..chunk_end], c);
+
+ off < end
+ } {}
+ }
+ }
+ }
+
+ /// Iterates over each row in the bitmap.
+ fn iter(&self) -> ChunksExact {
+ self.data.chunks_exact(self.size.width as usize)
+ }
+}
+
+/// A bitfield for VT text attributes.
+///
+/// It being a bitfield allows for simple diffing.
+#[repr(transparent)]
+#[derive(Default, Clone, Copy, PartialEq, Eq)]
+pub struct Attributes(u8);
+
+#[allow(non_upper_case_globals)]
+impl Attributes {
+ pub const None: Attributes = Attributes(0);
+ pub const Italic: Attributes = Attributes(0b1);
+ pub const Underlined: Attributes = Attributes(0b10);
+ pub const All: Attributes = Attributes(0b11);
+
+ pub const fn is(self, attr: Attributes) -> bool {
+ (self.0 & attr.0) == attr.0
+ }
+}
+
+unsafe impl MemsetSafe for Attributes {}
+
+impl BitOr for Attributes {
+ type Output = Attributes;
+
+ fn bitor(self, rhs: Self) -> Self::Output {
+ Attributes(self.0 | rhs.0)
+ }
+}
+
+impl BitXor for Attributes {
+ type Output = Attributes;
+
+ fn bitxor(self, rhs: Self) -> Self::Output {
+ Attributes(self.0 ^ rhs.0)
+ }
+}
+
+/// Stores VT attributes for the framebuffer.
+#[derive(Default)]
+struct AttributeBuffer {
+ data: Vec,
+ 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 {
+ 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 }
+ }
+}
diff --git a/pkgs/edit/src/fuzzy.rs b/pkgs/edit/src/fuzzy.rs
new file mode 100644
index 0000000..997ccda
--- /dev/null
+++ b/pkgs/edit/src/fuzzy.rs
@@ -0,0 +1,221 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! Fuzzy search algorithm based on the one used in VS Code (`/src/vs/base/common/fuzzyScorer.ts`).
+//! Other algorithms exist, such as Sublime Text's, or the one used in `fzf`,
+//! but I figured that this one is what lots of people may be familiar with.
+
+use std::vec;
+
+use crate::arena::{Arena, scratch_arena};
+use crate::icu;
+
+const NO_MATCH: i32 = 0;
+
+pub fn score_fuzzy<'a>(
+ arena: &'a Arena,
+ haystack: &str,
+ needle: &str,
+ allow_non_contiguous_matches: bool,
+) -> (i32, Vec) {
+ if haystack.is_empty() || needle.is_empty() {
+ // return early if target or query are empty
+ return (NO_MATCH, Vec::new_in(arena));
+ }
+
+ let scratch = scratch_arena(Some(arena));
+ let target = map_chars(&scratch, haystack);
+ let query = map_chars(&scratch, needle);
+
+ if target.len() < query.len() {
+ // impossible for query to be contained in target
+ return (NO_MATCH, Vec::new_in(arena));
+ }
+
+ let target_lower = icu::fold_case(&scratch, haystack);
+ let query_lower = icu::fold_case(&scratch, needle);
+ let target_lower = map_chars(&scratch, &target_lower);
+ let query_lower = map_chars(&scratch, &query_lower);
+
+ let area = query.len() * target.len();
+ let mut scores = vec::from_elem_in(0, area, &*scratch);
+ let mut matches = vec::from_elem_in(0, area, &*scratch);
+
+ //
+ // Build Scorer Matrix:
+ //
+ // The matrix is composed of query q and target t. For each index we score
+ // q[i] with t[i] and compare that with the previous score. If the score is
+ // equal or larger, we keep the match. In addition to the score, we also keep
+ // the length of the consecutive matches to use as boost for the score.
+ //
+ // t a r g e t
+ // q
+ // u
+ // e
+ // r
+ // y
+ //
+ for query_index in 0..query.len() {
+ let query_index_offset = query_index * target.len();
+ let query_index_previous_offset =
+ if query_index > 0 { (query_index - 1) * target.len() } else { 0 };
+
+ for target_index in 0..target.len() {
+ let current_index = query_index_offset + target_index;
+ let diag_index = if query_index > 0 && target_index > 0 {
+ query_index_previous_offset + target_index - 1
+ } else {
+ 0
+ };
+ let left_score = if target_index > 0 { scores[current_index - 1] } else { 0 };
+ let diag_score =
+ if query_index > 0 && target_index > 0 { scores[diag_index] } else { 0 };
+ let matches_sequence_len =
+ if query_index > 0 && target_index > 0 { matches[diag_index] } else { 0 };
+
+ // If we are not matching on the first query character any more, we only produce a
+ // score if we had a score previously for the last query index (by looking at the diagScore).
+ // This makes sure that the query always matches in sequence on the target. For example
+ // given a target of "ede" and a query of "de", we would otherwise produce a wrong high score
+ // for query[1] ("e") matching on target[0] ("e") because of the "beginning of word" boost.
+ let score = if diag_score == 0 && query_index != 0 {
+ 0
+ } else {
+ compute_char_score(
+ query[query_index],
+ query_lower[query_index],
+ if target_index != 0 { Some(target[target_index - 1]) } else { None },
+ target[target_index],
+ target_lower[target_index],
+ matches_sequence_len,
+ )
+ };
+
+ // We have a score and its equal or larger than the left score
+ // Match: sequence continues growing from previous diag value
+ // Score: increases by diag score value
+ let is_valid_score = score != 0 && diag_score + score >= left_score;
+ if is_valid_score
+ && (
+ // We don't need to check if it's contiguous if we allow non-contiguous matches
+ allow_non_contiguous_matches ||
+ // We must be looking for a contiguous match.
+ // Looking at an index higher than 0 in the query means we must have already
+ // found out this is contiguous otherwise there wouldn't have been a score
+ query_index > 0 ||
+ // lastly check if the query is completely contiguous at this index in the target
+ target_lower[target_index..].starts_with(&query_lower)
+ )
+ {
+ matches[current_index] = matches_sequence_len + 1;
+ scores[current_index] = diag_score + score;
+ } else {
+ // We either have no score or the score is lower than the left score
+ // Match: reset to 0
+ // Score: pick up from left hand side
+ matches[current_index] = NO_MATCH;
+ scores[current_index] = left_score;
+ }
+ }
+ }
+
+ // Restore Positions (starting from bottom right of matrix)
+ let mut positions = Vec::new_in(arena);
+
+ if !query.is_empty() && !target.is_empty() {
+ let mut query_index = query.len() - 1;
+ let mut target_index = target.len() - 1;
+
+ loop {
+ let current_index = query_index * target.len() + target_index;
+ if matches[current_index] == NO_MATCH {
+ if target_index == 0 {
+ break;
+ }
+ target_index -= 1; // go left
+ } else {
+ positions.push(target_index);
+
+ // go up and left
+ if query_index == 0 || target_index == 0 {
+ break;
+ }
+ query_index -= 1;
+ target_index -= 1;
+ }
+ }
+
+ positions.reverse();
+ }
+
+ (scores[area - 1], positions)
+}
+
+fn compute_char_score(
+ query: char,
+ query_lower: char,
+ target_prev: Option,
+ target_curr: char,
+ target_curr_lower: char,
+ matches_sequence_len: i32,
+) -> i32 {
+ let mut score = 0;
+
+ if !consider_as_equal(query_lower, target_curr_lower) {
+ return score; // no match of characters
+ }
+
+ // Character match bonus
+ score += 1;
+
+ // Consecutive match bonus
+ if matches_sequence_len > 0 {
+ score += matches_sequence_len * 5;
+ }
+
+ // Same case bonus
+ if query == target_curr {
+ score += 1;
+ }
+
+ if let Some(target_prev) = target_prev {
+ // After separator bonus
+ let separator_bonus = score_separator_at_pos(target_prev);
+ if separator_bonus > 0 {
+ score += separator_bonus;
+ }
+ // Inside word upper case bonus (camel case). We only give this bonus if we're not in a contiguous sequence.
+ // For example:
+ // NPE => NullPointerException = boost
+ // HTTP => HTTP = not boost
+ else if target_curr != target_curr_lower && matches_sequence_len == 0 {
+ score += 2;
+ }
+ } else {
+ // Start of word bonus
+ score += 8;
+ }
+
+ score
+}
+
+fn consider_as_equal(a: char, b: char) -> bool {
+ // Special case path separators: ignore platform differences
+ a == b || a == '/' || a == '\\' && b == '/' || b == '\\'
+}
+
+fn score_separator_at_pos(ch: char) -> i32 {
+ match ch {
+ '/' | '\\' => 5, // prefer path separators...
+ '_' | '-' | '.' | ' ' | '\'' | '"' | ':' => 4, // ...over other separators
+ _ => 0,
+ }
+}
+
+fn map_chars<'a>(arena: &'a Arena, s: &str) -> Vec {
+ let mut chars = Vec::with_capacity_in(s.len(), arena);
+ chars.extend(s.chars());
+ chars.shrink_to_fit();
+ chars
+}
diff --git a/pkgs/edit/src/hash.rs b/pkgs/edit/src/hash.rs
new file mode 100644
index 0000000..0634529
--- /dev/null
+++ b/pkgs/edit/src/hash.rs
@@ -0,0 +1,93 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! Provides fast, non-cryptographic hash functions.
+
+/// The venerable wyhash hash function.
+///
+/// It's fast, has good statistical properties, and is in the public domain.
+/// See:
+/// 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())
+}
diff --git a/pkgs/edit/src/helpers.rs b/pkgs/edit/src/helpers.rs
new file mode 100644
index 0000000..569218d
--- /dev/null
+++ b/pkgs/edit/src/helpers.rs
@@ -0,0 +1,277 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! Random assortment of helpers I didn't know where to put.
+
+use std::alloc::Allocator;
+use std::cmp::Ordering;
+use std::io::Read;
+use std::mem::{self, MaybeUninit};
+use std::ops::{Bound, Range, RangeBounds};
+use std::{fmt, ptr, slice, str};
+
+use crate::apperr;
+
+pub const KILO: usize = 1000;
+pub const MEGA: usize = 1000 * 1000;
+pub const GIGA: usize = 1000 * 1000 * 1000;
+
+pub const KIBI: usize = 1024;
+pub const MEBI: usize = 1024 * 1024;
+pub const GIBI: usize = 1024 * 1024 * 1024;
+
+pub struct MetricFormatter(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 = i32;
+
+/// To avoid overflow issues because you're adding two [`CoordType::MAX`] values together,
+/// you can use [`COORD_TYPE_SAFE_MIN`] and [`COORD_TYPE_SAFE_MAX`].
+pub const COORD_TYPE_SAFE_MAX: CoordType = 32767;
+
+/// See [`COORD_TYPE_SAFE_MAX`].
+pub const COORD_TYPE_SAFE_MIN: CoordType = -32767 - 1;
+
+/// A 2D point. Uses [`CoordType`].
+#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
+pub struct Point {
+ pub x: CoordType,
+ pub y: CoordType,
+}
+
+impl Point {
+ pub const MIN: Point = Point { x: CoordType::MIN, y: CoordType::MIN };
+ pub const MAX: Point = Point { x: CoordType::MAX, y: CoordType::MAX };
+}
+
+impl PartialOrd for Point {
+ fn partial_cmp(&self, other: &Self) -> Option {
+ Some(self.cmp(other))
+ }
+}
+
+impl Ord for Point {
+ fn cmp(&self, other: &Self) -> Ordering {
+ match self.y.cmp(&other.y) {
+ Ordering::Equal => self.x.cmp(&other.x),
+ ord => ord,
+ }
+ }
+}
+
+/// A 2D size. Uses [`CoordType`].
+#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
+pub struct Size {
+ pub width: CoordType,
+ pub height: CoordType,
+}
+
+impl Size {
+ pub fn as_rect(&self) -> Rect {
+ Rect { left: 0, top: 0, right: self.width, bottom: self.height }
+ }
+}
+
+/// A 2D rectangle. Uses [`CoordType`].
+#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
+pub struct Rect {
+ pub left: CoordType,
+ pub top: CoordType,
+ pub right: CoordType,
+ pub bottom: CoordType,
+}
+
+impl Rect {
+ /// Mimics CSS's `padding` property where `padding: a` is `a a a a`.
+ pub fn one(value: CoordType) -> Self {
+ Self { left: value, top: value, right: value, bottom: value }
+ }
+
+ /// Mimics CSS's `padding` property where `padding: a b` is `a b a b`,
+ /// and `a` is top/bottom and `b` is left/right.
+ pub fn two(top_bottom: CoordType, left_right: CoordType) -> Self {
+ Self { left: left_right, top: top_bottom, right: left_right, bottom: top_bottom }
+ }
+
+ /// Mimics CSS's `padding` property where `padding: a b c` is `a b c b`,
+ /// and `a` is top, `b` is left/right, and `c` is bottom.
+ pub fn three(top: CoordType, left_right: CoordType, bottom: CoordType) -> Self {
+ Self { left: left_right, top, right: left_right, bottom }
+ }
+
+ /// Is the rectangle empty?
+ pub fn is_empty(&self) -> bool {
+ self.left >= self.right || self.top >= self.bottom
+ }
+
+ /// Width of the rectangle.
+ pub fn width(&self) -> CoordType {
+ self.right - self.left
+ }
+
+ /// Height of the rectangle.
+ pub fn height(&self) -> CoordType {
+ self.bottom - self.top
+ }
+
+ /// Check if it contains a point.
+ pub fn contains(&self, point: Point) -> bool {
+ point.x >= self.left && point.x < self.right && point.y >= self.top && point.y < self.bottom
+ }
+
+ /// Intersect two rectangles.
+ pub fn intersect(&self, rhs: Self) -> Self {
+ let l = self.left.max(rhs.left);
+ let t = self.top.max(rhs.top);
+ let r = self.right.min(rhs.right);
+ let b = self.bottom.min(rhs.bottom);
+
+ // Ensure that the size is non-negative. This avoids bugs,
+ // because some height/width is negative all of a sudden.
+ let r = l.max(r);
+ let b = t.max(b);
+
+ Rect { left: l, top: t, right: r, bottom: b }
+ }
+}
+
+/// [`std::cmp::minmax`] is unstable, as per usual.
+pub fn minmax(v1: T, v2: T) -> [T; 2]
+where
+ T: Ord,
+{
+ if v2 < v1 { [v2, v1] } else { [v1, v2] }
+}
+
+#[inline(always)]
+#[allow(clippy::ptr_eq)]
+fn opt_ptr(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);
+ }
+}
+
+/// [`Read`] but with [`MaybeUninit`] buffers.
+pub fn file_read_uninit(
+ file: &mut T,
+ buf: &mut [MaybeUninit],
+) -> apperr::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)
+ }
+}
+
+/// 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()) }
+}
diff --git a/pkgs/edit/src/icu.rs b/pkgs/edit/src/icu.rs
new file mode 100644
index 0000000..0829970
--- /dev/null
+++ b/pkgs/edit/src/icu.rs
@@ -0,0 +1,1242 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! Bindings to the ICU library.
+
+use std::cmp::Ordering;
+use std::ffi::CStr;
+use std::mem;
+use std::mem::MaybeUninit;
+use std::ops::Range;
+use std::ptr::{null, null_mut};
+
+use crate::arena::{Arena, ArenaString, scratch_arena};
+use crate::buffer::TextBuffer;
+use crate::unicode::Utf8Chars;
+use crate::{apperr, arena_format, sys};
+
+static mut ENCODINGS: Vec<&'static str> = Vec::new();
+
+/// Returns a list of encodings ICU supports.
+pub fn get_available_encodings() -> &'static [&'static str] {
+ // OnceCell for people that want to put it into a static.
+ #[allow(static_mut_refs)]
+ unsafe {
+ if ENCODINGS.is_empty() {
+ if let Ok(f) = init_if_needed() {
+ let mut n = 0;
+ loop {
+ let name = (f.ucnv_getAvailableName)(n);
+ if name.is_null() {
+ break;
+ }
+ ENCODINGS.push(CStr::from_ptr(name).to_str().unwrap_unchecked());
+ n += 1;
+ }
+ }
+
+ if ENCODINGS.is_empty() {
+ ENCODINGS.push("UTF-8");
+ }
+ }
+ &ENCODINGS
+ }
+}
+
+/// Formats the given ICU error code into a human-readable string.
+pub fn apperr_format(f: &mut std::fmt::Formatter<'_>, code: u32) -> std::fmt::Result {
+ fn format(code: u32) -> &'static str {
+ let Ok(f) = init_if_needed() else {
+ return "";
+ };
+
+ let status = icu_ffi::UErrorCode::new(code);
+ let ptr = unsafe { (f.u_errorName)(status) };
+ if ptr.is_null() {
+ return "";
+ }
+
+ let str = unsafe { CStr::from_ptr(ptr) };
+ str.to_str().unwrap_or("")
+ }
+
+ let msg = format(code);
+ if !msg.is_empty() {
+ write!(f, "ICU Error: {msg}")
+ } else {
+ write!(f, "ICU Error: {code:#08x}")
+ }
+}
+
+/// Converts between two encodings using ICU.
+pub struct Converter<'pivot> {
+ source: *mut icu_ffi::UConverter,
+ 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,
+ ) -> apperr::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) -> ArenaString<'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],
+ ) -> apperr::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.
+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,
+}
+
+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) -> apperr::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 = tb.generation() as i64;
+
+ // ICU unfortunately expects a `UText` instance to have valid contents after construction.
+ utext_access(ut, 0, true);
+
+ 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();
+ }
+
+ unsafe {
+ let ut = &mut *ut_ptr;
+ let src_double_cache = double_cache_from_utext(src);
+ let dst_double_cache = double_cache_from_utext(ut);
+ let src_cache = &src_double_cache.cache[src_double_cache.mru as usize];
+ let dst_cache = &mut dst_double_cache.cache[dst_double_cache.mru as usize];
+
+ ut.provider_properties = src.provider_properties;
+ ut.chunk_native_limit = src.chunk_native_limit;
+ ut.native_indexing_limit = src.native_indexing_limit;
+ ut.chunk_native_start = src.chunk_native_start;
+ ut.chunk_offset = src.chunk_offset;
+ ut.chunk_length = src.chunk_length;
+ ut.chunk_contents = dst_cache.utf16.as_ptr();
+ ut.p_funcs = src.p_funcs;
+ ut.context = src.context;
+ ut.a = src.a;
+
+ // I wonder if it would make sense to use a Cow here. But probably not.
+ std::ptr::copy_nonoverlapping(src_cache, dst_cache, 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;
+ }
+ }
+
+ loop {
+ let Some(c) = it.next() else {
+ break;
+ };
+
+ // 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) -> apperr::Result {
+ let f = init_if_needed()?;
+ unsafe {
+ let scratch = scratch_arena(None);
+ let mut utf16 = Vec::new_in(&*scratch);
+ let mut status = icu_ffi::U_ZERO_ERROR;
+
+ utf16.extend(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: &Text) {
+ 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) };
+ }
+
+ /// Sets the regex to the absolute offset in the underlying text.
+ pub fn reset(&mut self, index: usize) {
+ let f = assume_loaded();
+ let mut status = icu_ffi::U_ZERO_ERROR;
+ unsafe { (f.uregex_reset64)(self.0, index as i64, &mut status) };
+ }
+}
+
+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;
+ }
+
+ let start = unsafe { (f.uregex_start64)(self.0, 0, &mut status) };
+ let end = unsafe { (f.uregex_end64)(self.0, 0, &mut status) };
+ if status.is_failure() {
+ return None;
+ }
+
+ let start = start.max(0);
+ let end = end.max(start);
+ Some(start as usize..end as usize)
+ }
+}
+
+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 {
+ // OnceCell for people that want to put it into a static.
+ #[allow(static_mut_refs)]
+ let coll = unsafe {
+ if ROOT_COLLATOR.is_none() {
+ ROOT_COLLATOR = Some(if let Ok(f) = init_if_needed() {
+ let mut status = icu_ffi::U_ZERO_ERROR;
+ (f.ucol_open)(c"".as_ptr(), &mut status)
+ } else {
+ null_mut()
+ });
+ }
+ 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) -> ArenaString<'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 = Vec::new_in(arena);
+ let mut output_len;
+
+ // First, guess the output length:
+ // TODO: What's a good heuristic here?
+ {
+ output.reserve_exact(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(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 { ArenaString::from_utf8_unchecked(output) };
+ }
+ }
+
+ let mut result = ArenaString::from_str(arena, input);
+ for b in unsafe { result.as_bytes_mut() } {
+ b.make_ascii_lowercase();
+ }
+ result
+}
+
+// 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,
+ ucnv_getAvailableName: icu_ffi::ucnv_getAvailableName,
+ ucnv_open: icu_ffi::ucnv_open,
+ ucnv_close: icu_ffi::ucnv_close,
+ ucnv_convertEx: icu_ffi::ucnv_convertEx,
+ ucasemap_open: icu_ffi::ucasemap_open,
+ ucasemap_utf8FoldCase: icu_ffi::ucasemap_utf8FoldCase,
+ utext_setup: icu_ffi::utext_setup,
+ utext_close: icu_ffi::utext_close,
+
+ // LIBICUI18N_PROC_NAMES
+ 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_start64: icu_ffi::uregex_start64,
+ uregex_end64: icu_ffi::uregex_end64,
+ ucol_open: icu_ffi::ucol_open,
+ ucol_strcollUTF8: icu_ffi::ucol_strcollUTF8,
+}
+
+const LIBICUUC_PROC_NAMES: [&CStr; 9] = [
+ // Found in libicuuc.so on UNIX, icuuc.dll/icu.dll on Windows.
+ c"u_errorName",
+ c"ucnv_getAvailableName",
+ c"ucnv_open",
+ c"ucnv_close",
+ c"ucnv_convertEx",
+ c"ucasemap_open",
+ c"ucasemap_utf8FoldCase",
+ c"utext_setup",
+ c"utext_close",
+];
+
+const LIBICUI18N_PROC_NAMES: [&CStr; 10] = [
+ // Found in libicui18n.so on UNIX, icuin.dll/icu.dll on Windows.
+ c"uregex_open",
+ c"uregex_close",
+ c"uregex_setTimeLimit",
+ c"uregex_setUText",
+ c"uregex_reset64",
+ c"uregex_findNext",
+ c"uregex_start64",
+ c"uregex_end64",
+ c"ucol_open",
+ c"ucol_strcollUTF8",
+];
+
+enum LibraryFunctionsState {
+ Uninitialized,
+ Failed,
+ Loaded(LibraryFunctions),
+}
+
+static mut LIBRARY_FUNCTIONS: LibraryFunctionsState = LibraryFunctionsState::Uninitialized;
+
+pub fn init() -> apperr::Result<()> {
+ init_if_needed()?;
+ Ok(())
+}
+
+#[allow(static_mut_refs)]
+fn init_if_needed() -> apperr::Result<&'static LibraryFunctions> {
+ #[cold]
+ fn load() {
+ unsafe {
+ LIBRARY_FUNCTIONS = LibraryFunctionsState::Failed;
+
+ let Ok(libicuuc) = sys::load_libicuuc() else {
+ return;
+ };
+ let Ok(libicui18n) = sys::load_libicui18n() else {
+ 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(unix)]
+ let scratch_outer = scratch_arena(None);
+ #[cfg(unix)]
+ let suffix = sys::icu_proc_suffix(&scratch_outer, libicuuc);
+
+ for (handle, names) in
+ [(libicuuc, &LIBICUUC_PROC_NAMES[..]), (libicui18n, &LIBICUI18N_PROC_NAMES[..])]
+ {
+ for name in names {
+ #[cfg(unix)]
+ let scratch = scratch_arena(Some(&scratch_outer));
+ #[cfg(unix)]
+ let name = &sys::add_icu_proc_suffix(&scratch, name, &suffix);
+
+ let Ok(func) = sys::get_proc_address(handle, name) else {
+ debug_assert!(
+ false,
+ "Failed to load ICU function: {}",
+ name.to_string_lossy()
+ );
+ 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(apperr::APP_ICU_MISSING),
+ }
+}
+
+#[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 crate::apperr;
+
+ #[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) -> apperr::Error {
+ debug_assert!(self.0 > 0);
+ apperr::Error::new_icu(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) -> *mut 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_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_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::*;
+
+ #[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);
+ }
+}
diff --git a/pkgs/edit/src/input.rs b/pkgs/edit/src/input.rs
new file mode 100644
index 0000000..88a32ac
--- /dev/null
+++ b/pkgs/edit/src/input.rs
@@ -0,0 +1,577 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! Parses VT sequences into input events.
+//!
+//! In the future this allows us to take apart the application and
+//! support input schemes that aren't VT, such as UEFI, or GUI.
+
+use crate::helpers::{CoordType, Point, Size};
+use crate::vt;
+
+/// Represents a key/modifier combination.
+///
+/// TODO: Is this a good idea? I did it to allow typing `kbmod::CTRL | vk::A`.
+/// The reason it's an awkard u32 and not a struct is to hopefully make ABIs easier later.
+/// Of course you could just translate on the ABI boundary, but my hope is that this
+/// design lets me realize some restrictions early on that I can't foresee yet.
+#[repr(transparent)]
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub struct InputKey(u32);
+
+impl InputKey {
+ pub(crate) const fn new(v: u32) -> Self {
+ Self(v)
+ }
+
+ pub(crate) const fn from_ascii(ch: char) -> Option {
+ if ch == ' ' || (ch >= '0' && ch <= '9') {
+ Some(Self(ch as u32))
+ } else if ch >= 'a' && ch <= 'z' {
+ Some(Self(ch as u32 & !0x20)) // Shift a-z to A-Z
+ } else if ch >= 'A' && ch <= 'Z' {
+ Some(Self(kbmod::SHIFT.0 | ch as u32))
+ } else {
+ None
+ }
+ }
+
+ pub(crate) const fn value(&self) -> u32 {
+ self.0
+ }
+
+ pub(crate) const fn key(&self) -> InputKey {
+ InputKey(self.0 & 0x00FFFFFF)
+ }
+
+ pub(crate) const fn modifiers(&self) -> InputKeyMod {
+ InputKeyMod(self.0 & 0xFF000000)
+ }
+
+ pub(crate) const fn modifiers_contains(&self, modifier: InputKeyMod) -> bool {
+ (self.0 & modifier.0) != 0
+ }
+
+ pub(crate) const fn with_modifiers(&self, modifiers: InputKeyMod) -> InputKey {
+ InputKey(self.0 | modifiers.0)
+ }
+}
+
+/// A keyboard modifier. Ctrl/Alt/Shift.
+#[repr(transparent)]
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub struct InputKeyMod(u32);
+
+impl InputKeyMod {
+ const fn new(v: u32) -> Self {
+ Self(v)
+ }
+
+ pub(crate) const fn contains(&self, modifier: InputKeyMod) -> bool {
+ (self.0 & modifier.0) != 0
+ }
+}
+
+impl std::ops::BitOr for InputKey {
+ type Output = InputKey;
+
+ fn bitor(self, rhs: InputKeyMod) -> InputKey {
+ InputKey(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);
+}
+
+/// Text input.
+///
+/// "Keyboard" input is also "text" input and vice versa.
+/// It differs in that text input can also be Unicode.
+#[derive(Clone, Copy)]
+pub struct InputText<'a> {
+ pub text: &'a str,
+ pub bracketed: bool,
+}
+
+/// Mouse input state. Up/Down, Left/Right, etc.
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
+pub enum InputMouseState {
+ #[default]
+ None,
+
+ // These 3 carry their state between frames.
+ Left,
+ Middle,
+ Right,
+
+ // These 2 get reset to None on the next frame.
+ Release,
+ Scroll,
+}
+
+/// Mouse input.
+#[derive(Clone, Copy)]
+pub struct InputMouse {
+ /// The state of the mouse.Up/Down, Left/Right, etc.
+ pub state: InputMouseState,
+ /// Any keyboard modifiers that are held down.
+ pub modifiers: InputKeyMod,
+ /// Position of the mouse in the viewport.
+ pub position: Point,
+ /// Scroll delta.
+ pub scroll: Point,
+}
+
+/// Primary result type of the parser.
+pub enum Input<'input> {
+ /// Window resize event.
+ Resize(Size),
+ /// Text input.
+ ///
+ /// Note that [`Input::Keyboard`] events can also be text.
+ Text(InputText<'input>),
+ /// Keyboard input.
+ Keyboard(InputKey),
+ /// Mouse input.
+ Mouse(InputMouse),
+}
+
+/// Parses VT sequences into input events.
+pub struct Parser {
+ bracketed_paste: bool,
+ x10_mouse_want: bool,
+ x10_mouse_buf: [u8; 3],
+ x10_mouse_len: usize,
+}
+
+impl Parser {
+ /// Creates a new parser that turns VT sequences into input events.
+ ///
+ /// Keep the instance alive for the lifetime of the input stream.
+ pub fn new() -> Self {
+ Self {
+ bracketed_paste: false,
+ x10_mouse_want: false,
+ x10_mouse_buf: [0; 3],
+ x10_mouse_len: 0,
+ }
+ }
+
+ /// Takes an [`vt::Stream`] and returns a [`Stream`]
+ /// that turns VT sequences into input events.
+ pub fn parse<'parser, 'vt, 'input>(
+ &'parser mut self,
+ stream: vt::Stream<'vt, 'input>,
+ ) -> Stream<'parser, 'vt, 'input> {
+ Stream { parser: self, stream }
+ }
+}
+
+/// An iterator that parses VT sequences into input events.
+///
+/// Can't implement [`Iterator`], because this is a "lending iterator".
+pub struct Stream<'parser, 'vt, 'input> {
+ parser: &'parser mut Parser,
+ stream: vt::Stream<'vt, 'input>,
+}
+
+impl<'input> Stream<'_, '_, 'input> {
+ #[allow(clippy::should_implement_trait)]
+ pub fn next(&mut self) -> Option> {
+ loop {
+ if self.parser.bracketed_paste {
+ return self.handle_bracketed_paste();
+ }
+
+ if self.parser.x10_mouse_want {
+ return self.parse_x10_mouse_coordinates();
+ }
+
+ match self.stream.next()? {
+ vt::Token::Text(text) => {
+ return Some(Input::Text(InputText { text, bracketed: false }));
+ }
+ vt::Token::Ctrl(ch) => match ch {
+ '\0' | '\t' | '\r' => return Some(Input::Keyboard(InputKey::new(ch as u32))),
+ '\n' => return Some(Input::Keyboard(kbmod::CTRL | vk::RETURN)),
+ ..='\x1a' => {
+ // Shift control code to A-Z
+ let key = ch as u32 | 0x40;
+ return Some(Input::Keyboard(kbmod::CTRL | InputKey::new(key)));
+ }
+ '\x7f' => return Some(Input::Keyboard(vk::BACK)),
+ _ => {}
+ },
+ vt::Token::Esc(ch) => {
+ match ch {
+ '\0' => return Some(Input::Keyboard(vk::ESCAPE)),
+ '\n' => return Some(Input::Keyboard(kbmod::CTRL_ALT | vk::RETURN)),
+ ' '..='~' => {
+ let ch = ch as u32;
+ let key = ch & !0x20; // Shift a-z to A-Z
+ let modifiers =
+ if (ch & 0x20) != 0 { kbmod::ALT } else { kbmod::ALT_SHIFT };
+ return Some(Input::Keyboard(modifiers | InputKey::new(key)));
+ }
+ _ => {}
+ }
+ }
+ vt::Token::SS3(ch) => {
+ if ('P'..='S').contains(&ch) {
+ let key = vk::F1.value() + ch as u32 - 'P' as u32;
+ return Some(Input::Keyboard(InputKey::new(key)));
+ }
+ }
+ vt::Token::Csi(csi) => {
+ match csi.final_byte {
+ 'A'..='H' => {
+ const LUT: [u8; 8] = [
+ vk::UP.value() as u8, // A
+ vk::DOWN.value() as u8, // B
+ vk::RIGHT.value() as u8, // C
+ vk::LEFT.value() as u8, // D
+ 0, // E
+ vk::END.value() as u8, // F
+ 0, // G
+ vk::HOME.value() as u8, // H
+ ];
+ let vk = LUT[csi.final_byte as usize - 'A' as usize];
+ if vk != 0 {
+ return Some(Input::Keyboard(
+ InputKey::new(vk as u32) | Self::parse_modifiers(csi),
+ ));
+ }
+ }
+ 'Z' => return Some(Input::Keyboard(kbmod::SHIFT | vk::TAB)),
+ '~' => {
+ const LUT: [u8; 35] = [
+ 0,
+ vk::HOME.value() as u8, // 1
+ vk::INSERT.value() as u8, // 2
+ vk::DELETE.value() as u8, // 3
+ vk::END.value() as u8, // 4
+ vk::PRIOR.value() as u8, // 5
+ vk::NEXT.value() as u8, // 6
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ vk::F5.value() as u8, // 15
+ 0,
+ vk::F6.value() as u8, // 17
+ vk::F7.value() as u8, // 18
+ vk::F8.value() as u8, // 19
+ vk::F9.value() as u8, // 20
+ vk::F10.value() as u8, // 21
+ 0,
+ vk::F11.value() as u8, // 23
+ vk::F12.value() as u8, // 24
+ vk::F13.value() as u8, // 25
+ vk::F14.value() as u8, // 26
+ 0,
+ vk::F15.value() as u8, // 28
+ vk::F16.value() as u8, // 29
+ 0,
+ vk::F17.value() as u8, // 31
+ vk::F18.value() as u8, // 32
+ vk::F19.value() as u8, // 33
+ vk::F20.value() as u8, // 34
+ ];
+ const LUT_LEN: u16 = LUT.len() as u16;
+
+ match csi.params[0] {
+ 0..LUT_LEN => {
+ let vk = LUT[csi.params[0] as usize];
+ if vk != 0 {
+ return Some(Input::Keyboard(
+ InputKey::new(vk as u32) | Self::parse_modifiers(csi),
+ ));
+ }
+ }
+ 200 => self.parser.bracketed_paste = true,
+ _ => {}
+ }
+ }
+ 'm' | 'M' if csi.private_byte == '<' => {
+ let btn = csi.params[0];
+ let mut mouse = InputMouse {
+ state: InputMouseState::None,
+ modifiers: kbmod::NONE,
+ position: Default::default(),
+ scroll: Default::default(),
+ };
+
+ mouse.state = InputMouseState::None;
+ if (btn & 0x40) != 0 {
+ mouse.state = InputMouseState::Scroll;
+ mouse.scroll.y += if (btn & 0x01) != 0 { 3 } else { -3 };
+ } else if csi.final_byte == 'M' {
+ const STATES: [InputMouseState; 4] = [
+ InputMouseState::Left,
+ InputMouseState::Middle,
+ InputMouseState::Right,
+ InputMouseState::None,
+ ];
+ mouse.state = STATES[(btn as usize) & 0x03];
+ }
+
+ mouse.modifiers = kbmod::NONE;
+ mouse.modifiers |=
+ if (btn & 0x04) != 0 { kbmod::SHIFT } else { kbmod::NONE };
+ mouse.modifiers |=
+ if (btn & 0x08) != 0 { kbmod::ALT } else { kbmod::NONE };
+ mouse.modifiers |=
+ if (btn & 0x10f) != 0 { kbmod::CTRL } else { kbmod::NONE };
+
+ mouse.position.x = csi.params[1] as CoordType - 1;
+ mouse.position.y = csi.params[2] as CoordType - 1;
+ return Some(Input::Mouse(mouse));
+ }
+ 'M' if csi.param_count == 0 => {
+ self.parser.x10_mouse_want = true;
+ }
+ 't' if csi.params[0] == 8 => {
+ // Window Size
+ let width = (csi.params[2] as CoordType).clamp(1, 32767);
+ let height = (csi.params[1] as CoordType).clamp(1, 32767);
+ return Some(Input::Resize(Size { width, height }));
+ }
+ _ => {}
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+
+ /// Once we encounter the start of a bracketed paste
+ /// we seek to the end of the paste in this function.
+ ///
+ /// A bracketed paste is basically:
+ /// ```text
+ /// [201~ lots of text [201~
+ /// ```
+ ///
+ /// That text inbetween is then expected to be taken literally.
+ /// It can inbetween be anything though, including other escape sequences.
+ /// This is the reason why this is a separate method.
+ #[cold]
+ fn handle_bracketed_paste(&mut self) -> Option> {
+ let beg = self.stream.offset();
+ let mut end = beg;
+
+ while let Some(token) = self.stream.next() {
+ if let vt::Token::Csi(csi) = token
+ && csi.final_byte == '~'
+ && csi.params[0] == 201
+ {
+ self.parser.bracketed_paste = false;
+ break;
+ }
+ end = self.stream.offset();
+ }
+
+ if end != beg {
+ let input = self.stream.input();
+ Some(Input::Text(InputText { text: &input[beg..end], bracketed: true }))
+ } else {
+ None
+ }
+ }
+
+ /// Implements the X10 mouse protocol via `CSI M CbCxCy`.
+ ///
+ /// You want to send numeric mouse coordinates.
+ /// You have CSI sequences with numeric parameters.
+ /// So, of course you put the coordinates as shifted ASCII characters after
+ /// the end of the sequence. Limited coordinate range and complicated parsing!
+ /// This is so puzzling to me. The existence of this function makes me unhappy.
+ #[cold]
+ fn parse_x10_mouse_coordinates(&mut self) -> Option> {
+ self.parser.x10_mouse_len +=
+ self.stream.read(&mut self.parser.x10_mouse_buf[self.parser.x10_mouse_len..]);
+ if self.parser.x10_mouse_len < 3 {
+ return None;
+ }
+
+ let button = self.parser.x10_mouse_buf[0] & 0b11;
+ let modifier = self.parser.x10_mouse_buf[0] & 0b11100;
+ let x = self.parser.x10_mouse_buf[1] as CoordType - 0x21;
+ let y = self.parser.x10_mouse_buf[2] as CoordType - 0x21;
+ let action = match button {
+ 0 => InputMouseState::Left,
+ 1 => InputMouseState::Middle,
+ 2 => InputMouseState::Right,
+ _ => InputMouseState::None,
+ };
+ let modifiers = match modifier {
+ 4 => kbmod::SHIFT,
+ 8 => kbmod::ALT,
+ 16 => kbmod::CTRL,
+ _ => kbmod::NONE,
+ };
+
+ self.parser.x10_mouse_want = false;
+ self.parser.x10_mouse_len = 0;
+
+ Some(Input::Mouse(InputMouse {
+ state: action,
+ modifiers,
+ position: Point { x, y },
+ scroll: Default::default(),
+ }))
+ }
+
+ fn parse_modifiers(csi: &vt::Csi) -> InputKeyMod {
+ let mut modifiers = kbmod::NONE;
+ let p1 = csi.params[1].saturating_sub(1);
+ if (p1 & 0x01) != 0 {
+ modifiers |= kbmod::SHIFT;
+ }
+ if (p1 & 0x02) != 0 {
+ modifiers |= kbmod::ALT;
+ }
+ if (p1 & 0x04) != 0 {
+ modifiers |= kbmod::CTRL;
+ }
+ modifiers
+ }
+}
diff --git a/pkgs/edit/src/lib.rs b/pkgs/edit/src/lib.rs
new file mode 100644
index 0000000..ed0b978
--- /dev/null
+++ b/pkgs/edit/src/lib.rs
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#![feature(
+ allocator_api,
+ breakpoint,
+ cold_path,
+ inherent_str_constructors,
+ let_chains,
+ linked_list_cursors,
+ maybe_uninit_fill,
+ maybe_uninit_slice,
+ maybe_uninit_uninit_array_transpose,
+ os_string_truncate
+)]
+#![allow(clippy::missing_transmute_annotations, clippy::new_without_default, stable_features)]
+
+#[macro_use]
+pub mod arena;
+
+pub mod apperr;
+pub mod base64;
+pub mod buffer;
+pub mod cell;
+pub mod document;
+pub mod framebuffer;
+pub mod hash;
+pub mod helpers;
+pub mod icu;
+pub mod input;
+pub mod oklab;
+pub mod path;
+pub mod simd;
+pub mod sys;
+pub mod tui;
+pub mod unicode;
+pub mod vt;
diff --git a/pkgs/edit/src/oklab.rs b/pkgs/edit/src/oklab.rs
new file mode 100644
index 0000000..5af68bf
--- /dev/null
+++ b/pkgs/edit/src/oklab.rs
@@ -0,0 +1,128 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! Oklab colorspace conversions.
+//!
+//! Implements Oklab as defined at:
+
+#![allow(clippy::excessive_precision)]
+
+/// An Oklab color with alpha.
+pub struct Lab {
+ pub l: f32,
+ pub a: f32,
+ pub b: f32,
+ pub alpha: f32,
+}
+
+/// Converts a 32-bit sRGB color to Oklab.
+pub fn srgb_to_oklab(color: u32) -> Lab {
+ let r = SRGB_TO_RGB_LUT[(color & 0xff) as usize];
+ let g = SRGB_TO_RGB_LUT[((color >> 8) & 0xff) as usize];
+ let b = SRGB_TO_RGB_LUT[((color >> 16) & 0xff) as usize];
+ let alpha = (color >> 24) as f32 * (1.0 / 255.0);
+
+ let l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
+ let m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
+ let s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
+
+ let l_ = cbrtf_est(l);
+ let m_ = cbrtf_est(m);
+ let s_ = cbrtf_est(s);
+
+ Lab {
+ l: 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
+ a: 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
+ b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
+ alpha,
+ }
+}
+
+/// Converts an Oklab color to a 32-bit sRGB color.
+pub fn oklab_to_srgb(c: Lab) -> u32 {
+ let l_ = c.l + 0.3963377774 * c.a + 0.2158037573 * c.b;
+ let m_ = c.l - 0.1055613458 * c.a - 0.0638541728 * c.b;
+ let s_ = c.l - 0.0894841775 * c.a - 1.2914855480 * c.b;
+
+ let l = l_ * l_ * l_;
+ let m = m_ * m_ * m_;
+ let s = s_ * s_ * s_;
+
+ let r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
+ let g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
+ let b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
+
+ let r = r.clamp(0.0, 1.0);
+ let g = g.clamp(0.0, 1.0);
+ let b = b.clamp(0.0, 1.0);
+ let alpha = c.alpha.clamp(0.0, 1.0);
+
+ let r = linear_to_srgb(r);
+ let g = linear_to_srgb(g);
+ let b = linear_to_srgb(b);
+ let a = (alpha * 255.0) as u32;
+
+ r | (g << 8) | (b << 16) | (a << 24)
+}
+
+/// Blends two 32-bit sRGB colors in the Oklab color space.
+pub fn oklab_blend(dst: u32, src: u32) -> u32 {
+ let dst = srgb_to_oklab(dst);
+ let src = srgb_to_oklab(src);
+
+ let inv_a = 1.0 - src.alpha;
+ let l = src.l + dst.l * inv_a;
+ let a = src.a + dst.a * inv_a;
+ let b = src.b + dst.b * inv_a;
+ let alpha = src.alpha + dst.alpha * inv_a;
+
+ oklab_to_srgb(Lab { l, a, b, alpha })
+}
+
+fn linear_to_srgb(c: f32) -> u32 {
+ (if c > 0.0031308 {
+ 255.0 * 1.055 * c.powf(1.0 / 2.4) - 255.0 * 0.055
+ } else {
+ 255.0 * 12.92 * c
+ }) as u32
+}
+
+#[inline]
+fn cbrtf_est(a: f32) -> f32 {
+ // http://metamerist.com/cbrt/cbrt.htm showed a great estimator for the cube root:
+ // f32_as_uint32_t / 3 + 709921077
+ // It's similar to the well known "fast inverse square root" trick.
+ // Lots of numbers around 709921077 perform at least equally well to 709921077,
+ // and it is unknown how and why 709921077 was chosen specifically.
+ let u: u32 = f32::to_bits(a); // evil f32ing point bit level hacking
+ let u = u / 3 + 709921077; // what the fuck?
+ let x: f32 = f32::from_bits(u);
+
+ // One round of Newton's method. It follows the Wikipedia article at
+ // https://en.wikipedia.org/wiki/Cube_root#Numerical_methods
+ // For `a`s in the range between 0 and 1, this results in a maximum error of
+ // less than 6.7e-4f, which is not good, but good enough for us, because
+ // we're not an image editor. The benefit is that it's really fast.
+ (1.0 / 3.0) * (a / (x * x) + (x + x)) // 1st iteration
+}
+
+#[rustfmt::skip]
+#[allow(clippy::excessive_precision)]
+const SRGB_TO_RGB_LUT: [f32; 256] = [
+ 0.0000000000, 0.0003035270, 0.0006070540, 0.0009105810, 0.0012141080, 0.0015176350, 0.0018211619, 0.0021246888, 0.0024282159, 0.0027317430, 0.0030352699, 0.0033465356, 0.0036765069, 0.0040247170, 0.0043914421, 0.0047769533,
+ 0.0051815170, 0.0056053917, 0.0060488326, 0.0065120910, 0.0069954102, 0.0074990317, 0.0080231922, 0.0085681248, 0.0091340570, 0.0097212177, 0.0103298230, 0.0109600937, 0.0116122449, 0.0122864870, 0.0129830306, 0.0137020806,
+ 0.0144438436, 0.0152085144, 0.0159962922, 0.0168073755, 0.0176419523, 0.0185002182, 0.0193823613, 0.0202885624, 0.0212190095, 0.0221738834, 0.0231533647, 0.0241576303, 0.0251868572, 0.0262412224, 0.0273208916, 0.0284260381,
+ 0.0295568332, 0.0307134409, 0.0318960287, 0.0331047624, 0.0343398079, 0.0356013142, 0.0368894450, 0.0382043645, 0.0395462364, 0.0409151986, 0.0423114114, 0.0437350273, 0.0451862030, 0.0466650836, 0.0481718220, 0.0497065634,
+ 0.0512694679, 0.0528606549, 0.0544802807, 0.0561284944, 0.0578054339, 0.0595112406, 0.0612460710, 0.0630100295, 0.0648032799, 0.0666259527, 0.0684781820, 0.0703601092, 0.0722718611, 0.0742135793, 0.0761853904, 0.0781874284,
+ 0.0802198276, 0.0822827145, 0.0843762159, 0.0865004659, 0.0886556059, 0.0908417329, 0.0930589810, 0.0953074843, 0.0975873619, 0.0998987406, 0.1022417471, 0.1046164930, 0.1070231125, 0.1094617173, 0.1119324341, 0.1144353822,
+ 0.1169706732, 0.1195384338, 0.1221387982, 0.1247718409, 0.1274376959, 0.1301364899, 0.1328683347, 0.1356333494, 0.1384316236, 0.1412633061, 0.1441284865, 0.1470272839, 0.1499598026, 0.1529261619, 0.1559264660, 0.1589608639,
+ 0.1620294005, 0.1651322246, 0.1682693958, 0.1714410931, 0.1746473908, 0.1778884083, 0.1811642349, 0.1844749898, 0.1878207624, 0.1912016720, 0.1946178079, 0.1980693042, 0.2015562356, 0.2050787061, 0.2086368501, 0.2122307271,
+ 0.2158605307, 0.2195262313, 0.2232279778, 0.2269658893, 0.2307400703, 0.2345506549, 0.2383976579, 0.2422811985, 0.2462013960, 0.2501583695, 0.2541521788, 0.2581829131, 0.2622507215, 0.2663556635, 0.2704978585, 0.2746773660,
+ 0.2788943350, 0.2831487954, 0.2874408960, 0.2917706966, 0.2961383164, 0.3005438447, 0.3049873710, 0.3094689548, 0.3139887452, 0.3185468316, 0.3231432438, 0.3277781308, 0.3324515820, 0.3371636569, 0.3419144452, 0.3467040956,
+ 0.3515326977, 0.3564002514, 0.3613068759, 0.3662526906, 0.3712377846, 0.3762622178, 0.3813261092, 0.3864295185, 0.3915725648, 0.3967553079, 0.4019778669, 0.4072403014, 0.4125427008, 0.4178851545, 0.4232677519, 0.4286905527,
+ 0.4341537058, 0.4396572411, 0.4452012479, 0.4507858455, 0.4564110637, 0.4620770514, 0.4677838385, 0.4735315442, 0.4793202281, 0.4851499796, 0.4910208881, 0.4969330430, 0.5028865933, 0.5088814497, 0.5149177909, 0.5209956765,
+ 0.5271152258, 0.5332764983, 0.5394796133, 0.5457245708, 0.5520114899, 0.5583404899, 0.5647116303, 0.5711249113, 0.5775805116, 0.5840784907, 0.5906189084, 0.5972018838, 0.6038274169, 0.6104956269, 0.6172066331, 0.6239604354,
+ 0.6307572126, 0.6375969648, 0.6444797516, 0.6514056921, 0.6583748460, 0.6653873324, 0.6724432111, 0.6795425415, 0.6866854429, 0.6938719153, 0.7011020184, 0.7083759308, 0.7156936526, 0.7230552435, 0.7304608822, 0.7379105687,
+ 0.7454043627, 0.7529423237, 0.7605246305, 0.7681512833, 0.7758223414, 0.7835379243, 0.7912980318, 0.7991028428, 0.8069523573, 0.8148466945, 0.8227858543, 0.8307699561, 0.8387991190, 0.8468732834, 0.8549926877, 0.8631572723,
+ 0.8713672161, 0.8796223402, 0.8879231811, 0.8962693810, 0.9046613574, 0.9130986929, 0.9215820432, 0.9301108718, 0.9386858940, 0.9473065734, 0.9559735060, 0.9646862745, 0.9734454751, 0.9822505713, 0.9911022186, 1.0000000000,
+];
diff --git a/pkgs/edit/src/path.rs b/pkgs/edit/src/path.rs
new file mode 100644
index 0000000..45ba6ac
--- /dev/null
+++ b/pkgs/edit/src/path.rs
@@ -0,0 +1,82 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! Path related helpers.
+
+use std::ffi::OsStr;
+use std::path::{Component, MAIN_SEPARATOR_STR, Path, PathBuf};
+
+/// Normalizes a given path by removing redundant components.
+/// The given path must be absolute (e.g. by joining it with the current working directory).
+pub fn normalize(path: &Path) -> PathBuf {
+ debug_assert!(path.is_absolute());
+
+ let mut res = PathBuf::with_capacity(path.as_os_str().len());
+ let mut root_len = 0;
+
+ for component in path.components() {
+ match component {
+ Component::Prefix(p) => res.push(p.as_os_str()),
+ Component::RootDir => {
+ res.push(OsStr::new(MAIN_SEPARATOR_STR));
+ root_len = res.as_os_str().len();
+ }
+ Component::CurDir => {}
+ Component::ParentDir => {
+ // Get the length up to the parent directory
+ if let Some(len) = res
+ .parent()
+ .map(|p| p.as_os_str().as_encoded_bytes().len())
+ // Ensure we don't pop the root directory
+ && len >= root_len
+ {
+ res.as_mut_os_string().truncate(len);
+ }
+ }
+ Component::Normal(p) => res.push(p),
+ }
+ }
+
+ res
+}
+
+#[cfg(test)]
+mod tests {
+ use std::ffi::OsString;
+ use std::path::Path;
+
+ use super::*;
+
+ fn norm(s: &str) -> OsString {
+ normalize(Path::new(s)).into_os_string()
+ }
+
+ #[cfg(unix)]
+ #[test]
+ fn test_unix() {
+ assert_eq!(norm("/a/b/c"), "/a/b/c");
+ assert_eq!(norm("/a/b/c/"), "/a/b/c");
+ assert_eq!(norm("/a/./b"), "/a/b");
+ assert_eq!(norm("/a/b/../c"), "/a/c");
+ assert_eq!(norm("/../../a"), "/a");
+ assert_eq!(norm("/../"), "/");
+ assert_eq!(norm("/a//b/c"), "/a/b/c");
+ assert_eq!(norm("/a/b/c/../../../../d"), "/d");
+ assert_eq!(norm("//"), "/");
+ }
+
+ #[cfg(windows)]
+ #[test]
+ fn test_windows() {
+ assert_eq!(norm(r"C:\a\b\c"), r"C:\a\b\c");
+ assert_eq!(norm(r"C:\a\b\c\"), r"C:\a\b\c");
+ assert_eq!(norm(r"C:\a\.\b"), r"C:\a\b");
+ assert_eq!(norm(r"C:\a\b\..\c"), r"C:\a\c");
+ assert_eq!(norm(r"C:\..\..\a"), r"C:\a");
+ assert_eq!(norm(r"C:\..\"), r"C:\");
+ assert_eq!(norm(r"C:\a\\b\c"), r"C:\a\b\c");
+ assert_eq!(norm(r"C:/a\b/c"), r"C:\a\b\c");
+ assert_eq!(norm(r"C:\a\b\c\..\..\..\..\d"), r"C:\d");
+ assert_eq!(norm(r"\\server\share\path"), r"\\server\share\path");
+ }
+}
diff --git a/pkgs/edit/src/simd/memchr2.rs b/pkgs/edit/src/simd/memchr2.rs
new file mode 100644
index 0000000..be2980e
--- /dev/null
+++ b/pkgs/edit/src/simd/memchr2.rs
@@ -0,0 +1,196 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! `memchr`, but with two needles.
+
+use std::ptr;
+
+use super::distance;
+
+/// `memchr`, but with two needles.
+///
+/// Returns the index of the first occurrence of either needle in the
+/// `haystack`. If no needle is found, `haystack.len()` is returned.
+/// `offset` specifies the index to start searching from.
+pub fn memchr2(needle1: u8, needle2: u8, haystack: &[u8], offset: usize) -> usize {
+ unsafe {
+ let beg = haystack.as_ptr();
+ let end = beg.add(haystack.len());
+ let it = beg.add(offset.min(haystack.len()));
+ let it = memchr2_raw(needle1, needle2, it, end);
+ distance(it, beg)
+ }
+}
+
+unsafe fn memchr2_raw(needle1: u8, needle2: u8, beg: *const u8, end: *const u8) -> *const u8 {
+ #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+ return unsafe { MEMCHR2_DISPATCH(needle1, needle2, beg, end) };
+
+ #[cfg(target_arch = "aarch64")]
+ return unsafe { memchr2_neon(needle1, needle2, beg, end) };
+}
+
+unsafe fn memchr2_fallback(
+ needle1: u8,
+ needle2: u8,
+ mut beg: *const u8,
+ end: *const u8,
+) -> *const u8 {
+ unsafe {
+ while !ptr::eq(beg, end) {
+ let ch = *beg;
+ if ch == needle1 || ch == needle2 {
+ break;
+ }
+ beg = beg.add(1);
+ }
+ beg
+ }
+}
+
+// In order to make `memchr2_raw` slim and fast, we use a function pointer that updates
+// itself to the correct implementation on the first call. This reduces binary size.
+// It would also reduce branches if we had >2 implementations (a jump still needs to be predicted).
+// NOTE that this ONLY works if Control Flow Guard is disabled on Windows.
+#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+static mut MEMCHR2_DISPATCH: unsafe fn(
+ needle1: u8,
+ needle2: u8,
+ beg: *const u8,
+ end: *const u8,
+) -> *const u8 = memchr2_dispatch;
+
+#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+unsafe fn memchr2_dispatch(needle1: u8, needle2: u8, beg: *const u8, end: *const u8) -> *const u8 {
+ let func = if is_x86_feature_detected!("avx2") { memchr2_avx2 } else { memchr2_fallback };
+ unsafe { MEMCHR2_DISPATCH = func };
+ unsafe { func(needle1, needle2, beg, end) }
+}
+
+// FWIW, I found that adding support for AVX512 was not useful at the time,
+// as it only marginally improved file load performance by <5%.
+#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+#[target_feature(enable = "avx2")]
+unsafe fn memchr2_avx2(needle1: u8, needle2: u8, mut beg: *const u8, end: *const u8) -> *const u8 {
+ unsafe {
+ #[cfg(target_arch = "x86")]
+ use std::arch::x86::*;
+ #[cfg(target_arch = "x86_64")]
+ use std::arch::x86_64::*;
+
+ let n1 = _mm256_set1_epi8(needle1 as i8);
+ let n2 = _mm256_set1_epi8(needle2 as i8);
+ let mut remaining = distance(end, beg);
+
+ while remaining >= 32 {
+ let v = _mm256_loadu_si256(beg as *const _);
+ let a = _mm256_cmpeq_epi8(v, n1);
+ let b = _mm256_cmpeq_epi8(v, n2);
+ let c = _mm256_or_si256(a, b);
+ let m = _mm256_movemask_epi8(c) as u32;
+
+ if m != 0 {
+ return beg.add(m.trailing_zeros() as usize);
+ }
+
+ beg = beg.add(32);
+ remaining -= 32;
+ }
+
+ memchr2_fallback(needle1, needle2, beg, end)
+ }
+}
+
+#[cfg(target_arch = "aarch64")]
+unsafe fn memchr2_neon(needle1: u8, needle2: u8, mut beg: *const u8, end: *const u8) -> *const u8 {
+ unsafe {
+ use std::arch::aarch64::*;
+
+ if distance(end, beg) >= 16 {
+ let n1 = vdupq_n_u8(needle1);
+ let n2 = vdupq_n_u8(needle2);
+
+ loop {
+ let v = vld1q_u8(beg as *const _);
+ let a = vceqq_u8(v, n1);
+ let b = vceqq_u8(v, n2);
+ let c = vorrq_u8(a, b);
+
+ // https://community.arm.com/arm-community-blogs/b/servers-and-cloud-computing-blog/posts/porting-x86-vector-bitmask-optimizations-to-arm-neon
+ let m = vreinterpretq_u16_u8(c);
+ let m = vshrn_n_u16(m, 4);
+ let m = vreinterpret_u64_u8(m);
+ let m = vget_lane_u64(m, 0);
+
+ if m != 0 {
+ return beg.add(m.trailing_zeros() as usize >> 2);
+ }
+
+ beg = beg.add(16);
+ if distance(end, beg) < 16 {
+ break;
+ }
+ }
+ }
+
+ memchr2_fallback(needle1, needle2, beg, end)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::slice;
+
+ use super::*;
+ use crate::sys;
+
+ #[test]
+ fn test_empty() {
+ assert_eq!(memchr2(b'a', b'b', b"", 0), 0);
+ }
+
+ #[test]
+ fn test_basic() {
+ let haystack = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ let haystack = &haystack[..43];
+
+ assert_eq!(memchr2(b'a', b'z', haystack, 0), 0);
+ assert_eq!(memchr2(b'p', b'q', haystack, 0), 15);
+ assert_eq!(memchr2(b'Q', b'Z', haystack, 0), 42);
+ assert_eq!(memchr2(b'0', b'9', haystack, 0), haystack.len());
+ }
+
+ // Test that it doesn't match before/after the start offset respectively.
+ #[test]
+ fn test_with_offset() {
+ let haystack = b"abcdefghabcdefghabcdefghabcdefghabcdefgh";
+
+ assert_eq!(memchr2(b'a', b'b', haystack, 0), 0);
+ assert_eq!(memchr2(b'a', b'b', haystack, 1), 1);
+ assert_eq!(memchr2(b'a', b'b', haystack, 2), 8);
+ assert_eq!(memchr2(b'a', b'b', haystack, 9), 9);
+ assert_eq!(memchr2(b'a', b'b', haystack, 16), 16);
+ assert_eq!(memchr2(b'a', b'b', haystack, 41), 40);
+ }
+
+ // Test memory access safety at page boundaries.
+ // The test is a success if it doesn't segfault.
+ #[test]
+ fn test_page_boundary() {
+ let page = unsafe {
+ let page_size = 4096;
+
+ // 3 pages: uncommitted, committed, uncommitted
+ let ptr = sys::virtual_reserve(page_size * 3).unwrap();
+ sys::virtual_commit(ptr.add(page_size), page_size).unwrap();
+ slice::from_raw_parts_mut(ptr.add(page_size).as_ptr(), page_size)
+ };
+
+ page.fill(b'a');
+
+ // Test if it seeks beyond the page boundary.
+ assert_eq!(memchr2(b'\0', b'\0', &page[page.len() - 40..], 0), 40);
+ // Test if it seeks before the page boundary for the masked/partial load.
+ assert_eq!(memchr2(b'\0', b'\0', &page[..10], 0), 10);
+ }
+}
diff --git a/pkgs/edit/src/simd/memrchr2.rs b/pkgs/edit/src/simd/memrchr2.rs
new file mode 100644
index 0000000..26a5a55
--- /dev/null
+++ b/pkgs/edit/src/simd/memrchr2.rs
@@ -0,0 +1,196 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! `memchr`, but with two needles.
+
+use std::ptr;
+
+use super::distance;
+
+/// `memchr`, but with two needles.
+///
+/// If no needle is found, 0 is returned.
+/// Unlike `memchr2` (or `memrchr`), an offset PAST the hit is returned.
+/// This is because this function is primarily used for
+/// `ucd::newlines_backward`, which needs exactly that.
+pub fn memrchr2(needle1: u8, needle2: u8, haystack: &[u8], offset: usize) -> Option {
+ unsafe {
+ let beg = haystack.as_ptr();
+ let it = beg.add(offset.min(haystack.len()));
+ let it = memrchr2_raw(needle1, needle2, beg, it);
+ if it.is_null() { None } else { Some(distance(it, beg)) }
+ }
+}
+
+unsafe fn memrchr2_raw(needle1: u8, needle2: u8, beg: *const u8, end: *const u8) -> *const u8 {
+ #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+ return unsafe { MEMRCHR2_DISPATCH(needle1, needle2, beg, end) };
+
+ #[cfg(target_arch = "aarch64")]
+ return unsafe { memrchr2_neon(needle1, needle2, beg, end) };
+
+ #[allow(unreachable_code)]
+ return unsafe { memrchr2_fallback(needle1, needle2, beg, end) };
+}
+
+unsafe fn memrchr2_fallback(
+ needle1: u8,
+ needle2: u8,
+ beg: *const u8,
+ mut end: *const u8,
+) -> *const u8 {
+ unsafe {
+ while !ptr::eq(end, beg) {
+ end = end.sub(1);
+ let ch = *end;
+ if ch == needle1 || needle2 == ch {
+ return end;
+ }
+ }
+ ptr::null()
+ }
+}
+
+#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+static mut MEMRCHR2_DISPATCH: unsafe fn(
+ needle1: u8,
+ needle2: u8,
+ beg: *const u8,
+ end: *const u8,
+) -> *const u8 = memrchr2_dispatch;
+
+#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+unsafe fn memrchr2_dispatch(needle1: u8, needle2: u8, beg: *const u8, end: *const u8) -> *const u8 {
+ let func = if is_x86_feature_detected!("avx2") { memrchr2_avx2 } else { memrchr2_fallback };
+ unsafe { MEMRCHR2_DISPATCH = func };
+ unsafe { func(needle1, needle2, beg, end) }
+}
+
+#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+#[target_feature(enable = "avx2")]
+unsafe fn memrchr2_avx2(needle1: u8, needle2: u8, beg: *const u8, mut end: *const u8) -> *const u8 {
+ unsafe {
+ #[cfg(target_arch = "x86")]
+ use std::arch::x86::*;
+ #[cfg(target_arch = "x86_64")]
+ use std::arch::x86_64::*;
+
+ if distance(end, beg) >= 32 {
+ let n1 = _mm256_set1_epi8(needle1 as i8);
+ let n2 = _mm256_set1_epi8(needle2 as i8);
+
+ loop {
+ end = end.sub(32);
+
+ let v = _mm256_loadu_si256(end as *const _);
+ let a = _mm256_cmpeq_epi8(v, n1);
+ let b = _mm256_cmpeq_epi8(v, n2);
+ let c = _mm256_or_si256(a, b);
+ let m = _mm256_movemask_epi8(c) as u32;
+
+ if m != 0 {
+ return end.add(31 - m.leading_zeros() as usize);
+ }
+
+ if distance(end, beg) < 32 {
+ break;
+ }
+ }
+ }
+
+ memrchr2_fallback(needle1, needle2, beg, end)
+ }
+}
+
+#[cfg(target_arch = "aarch64")]
+unsafe fn memrchr2_neon(needle1: u8, needle2: u8, beg: *const u8, mut end: *const u8) -> *const u8 {
+ unsafe {
+ use std::arch::aarch64::*;
+
+ if distance(end, beg) >= 16 {
+ let n1 = vdupq_n_u8(needle1);
+ let n2 = vdupq_n_u8(needle2);
+
+ loop {
+ end = end.sub(16);
+
+ let v = vld1q_u8(end as *const _);
+ let a = vceqq_u8(v, n1);
+ let b = vceqq_u8(v, n2);
+ let c = vorrq_u8(a, b);
+
+ // https://community.arm.com/arm-community-blogs/b/servers-and-cloud-computing-blog/posts/porting-x86-vector-bitmask-optimizations-to-arm-neon
+ let m = vreinterpretq_u16_u8(c);
+ let m = vshrn_n_u16(m, 4);
+ let m = vreinterpret_u64_u8(m);
+ let m = vget_lane_u64(m, 0);
+
+ if m != 0 {
+ return end.add(15 - (m.leading_zeros() as usize >> 2));
+ }
+
+ if distance(end, beg) < 16 {
+ break;
+ }
+ }
+ }
+
+ memrchr2_fallback(needle1, needle2, beg, end)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::slice;
+
+ use super::*;
+ use crate::sys;
+
+ #[test]
+ fn test_empty() {
+ assert_eq!(memrchr2(b'a', b'b', b"", 0), None);
+ }
+
+ #[test]
+ fn test_basic() {
+ let haystack = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ let haystack = &haystack[..43];
+
+ assert_eq!(memrchr2(b'Q', b'P', haystack, 43), Some(42));
+ assert_eq!(memrchr2(b'p', b'o', haystack, 43), Some(15));
+ assert_eq!(memrchr2(b'a', b'b', haystack, 43), Some(1));
+ assert_eq!(memrchr2(b'0', b'9', haystack, 43), None);
+ }
+
+ // Test that it doesn't match before/after the start offset respectively.
+ #[test]
+ fn test_with_offset() {
+ let haystack = b"abcdefghabcdefghabcdefghabcdefghabcdefgh";
+
+ assert_eq!(memrchr2(b'h', b'g', haystack, 40), Some(39));
+ assert_eq!(memrchr2(b'h', b'g', haystack, 39), Some(38));
+ assert_eq!(memrchr2(b'a', b'b', haystack, 9), Some(8));
+ assert_eq!(memrchr2(b'a', b'b', haystack, 1), Some(0));
+ assert_eq!(memrchr2(b'a', b'b', haystack, 0), None);
+ }
+
+ // Test memory access safety at page boundaries.
+ // The test is a success if it doesn't segfault.
+ #[test]
+ fn test_page_boundary() {
+ let page = unsafe {
+ let page_size = 4096;
+
+ // 3 pages: uncommitted, committed, uncommitted
+ let ptr = sys::virtual_reserve(page_size * 3).unwrap();
+ sys::virtual_commit(ptr.add(page_size), page_size).unwrap();
+ slice::from_raw_parts_mut(ptr.add(page_size).as_ptr(), page_size)
+ };
+
+ page.fill(b'a');
+
+ // Same as above, but for memrchr2 (hence reversed).
+ assert_eq!(memrchr2(b'\0', b'\0', &page[page.len() - 10..], 10), None);
+ assert_eq!(memrchr2(b'\0', b'\0', &page[..40], 40), None);
+ }
+}
diff --git a/pkgs/edit/src/simd/memset.rs b/pkgs/edit/src/simd/memset.rs
new file mode 100644
index 0000000..028b66b
--- /dev/null
+++ b/pkgs/edit/src/simd/memset.rs
@@ -0,0 +1,345 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! `memchr` for arbitrary sizes (1/2/4/8 bytes).
+//!
+//! Clang calls the C `memset` function only for byte-sized types (or 0 fills).
+//! We however need to fill other types as well. For that, clang generates
+//! SIMD loops under higher optimization levels. With `-Os` however, it only
+//! generates a trivial loop which is too slow for our needs.
+//!
+//! This implementation uses SWAR to only have a single implementation for all
+//! 4 sizes: By duplicating smaller types into a larger `u64` register we can
+//! treat all sizes as if they were `u64`. The only thing we need to take care
+//! of is the tail end of the array, which needs to write 0-7 additional bytes.
+
+use std::mem;
+
+use super::distance;
+
+/// A marker trait for types that are safe to `memset`.
+///
+/// # Safety
+///
+/// Just like with C's `memset`, bad things happen
+/// if you use this with non-trivial types.
+pub unsafe trait MemsetSafe: Copy {}
+
+unsafe impl MemsetSafe for u8 {}
+unsafe impl MemsetSafe for u16 {}
+unsafe impl MemsetSafe for u32 {}
+unsafe impl MemsetSafe for u64 {}
+unsafe impl MemsetSafe for usize {}
+
+unsafe impl MemsetSafe for i8 {}
+unsafe impl MemsetSafe for i16 {}
+unsafe impl MemsetSafe for i32 {}
+unsafe impl MemsetSafe for i64 {}
+unsafe impl MemsetSafe for isize {}
+
+/// Fills a slice with the given value.
+#[inline]
+pub fn memset(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);
+ }
+ _ => unreachable!(),
+ }
+ }
+}
+
+#[inline]
+fn memset_raw(beg: *mut u8, end: *mut u8, val: u64) {
+ #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+ return unsafe { MEMSET_DISPATCH(beg, end, val) };
+
+ #[cfg(target_arch = "aarch64")]
+ return unsafe { memset_neon(beg, end, val) };
+}
+
+#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+static mut MEMSET_DISPATCH: unsafe fn(beg: *mut u8, end: *mut u8, val: u64) = memset_dispatch;
+
+#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+fn memset_dispatch(beg: *mut u8, end: *mut u8, val: u64) {
+ let func = if is_x86_feature_detected!("avx2") { memset_avx2 } else { memset_sse2 };
+ unsafe { MEMSET_DISPATCH = func };
+ unsafe { func(beg, end, val) }
+}
+
+#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+#[target_feature(enable = "sse2")]
+unsafe fn memset_sse2(mut beg: *mut u8, end: *mut u8, val: u64) {
+ unsafe {
+ #[cfg(target_arch = "x86")]
+ use std::arch::x86::*;
+ #[cfg(target_arch = "x86_64")]
+ use std::arch::x86_64::*;
+
+ let mut remaining = distance(end, beg);
+
+ if remaining >= 16 {
+ let fill = _mm_set1_epi64x(val as i64);
+
+ while remaining >= 32 {
+ _mm_storeu_si128(beg as *mut _, fill);
+ _mm_storeu_si128(beg.add(16) as *mut _, fill);
+
+ beg = beg.add(32);
+ remaining -= 32;
+ }
+
+ if remaining >= 16 {
+ // 16-31 bytes remaining
+ _mm_storeu_si128(beg as *mut _, fill);
+ _mm_storeu_si128(end.sub(16) as *mut _, fill);
+ return;
+ }
+ }
+
+ if remaining >= 8 {
+ // 8-15 bytes remaining
+ (beg as *mut u64).write_unaligned(val);
+ (end.sub(8) as *mut u64).write_unaligned(val);
+ } else if remaining >= 4 {
+ // 4-7 bytes remaining
+ (beg as *mut u32).write_unaligned(val as u32);
+ (end.sub(4) as *mut u32).write_unaligned(val as u32);
+ } else if remaining >= 2 {
+ // 2-3 bytes remaining
+ (beg as *mut u16).write_unaligned(val as u16);
+ (end.sub(2) as *mut u16).write_unaligned(val as u16);
+ } else if remaining >= 1 {
+ // 1 byte remaining
+ beg.write(val as u8);
+ }
+ }
+}
+
+#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+#[target_feature(enable = "avx2")]
+fn memset_avx2(mut beg: *mut u8, end: *mut u8, val: u64) {
+ unsafe {
+ #[cfg(target_arch = "x86")]
+ use std::arch::x86::*;
+ #[cfg(target_arch = "x86_64")]
+ use std::arch::x86_64::*;
+ use std::hint::black_box;
+
+ let mut remaining = distance(end, beg);
+
+ if remaining >= 128 {
+ let fill = _mm256_set1_epi64x(val as i64);
+
+ loop {
+ _mm256_storeu_si256(beg as *mut _, fill);
+ _mm256_storeu_si256(beg.add(32) as *mut _, fill);
+ _mm256_storeu_si256(beg.add(64) as *mut _, fill);
+ _mm256_storeu_si256(beg.add(96) as *mut _, fill);
+
+ beg = beg.add(128);
+ remaining -= 128;
+ if remaining < 128 {
+ break;
+ }
+ }
+ }
+
+ if remaining >= 16 {
+ let fill = _mm_set1_epi64x(val as i64);
+
+ loop {
+ // LLVM is _very_ eager to unroll loops. In the absence of an unroll attribute, black_box does the job.
+ // Note that this must not be applied to the intrinsic parameters, as they're otherwise misoptimized.
+ #[allow(clippy::unit_arg)]
+ black_box(_mm_storeu_si128(beg as *mut _, fill));
+
+ beg = beg.add(16);
+ remaining -= 16;
+ if remaining < 16 {
+ break;
+ }
+ }
+ }
+
+ // `remaining` is between 0 and 15 at this point.
+ // By overlapping the stores we can write all of them in at most 2 stores. This approach
+ // can be seen in various libraries, such as wyhash which uses it for loading data in `wyr3`.
+ if remaining >= 8 {
+ // 8-15 bytes
+ (beg as *mut u64).write_unaligned(val);
+ (end.sub(8) as *mut u64).write_unaligned(val);
+ } else if remaining >= 4 {
+ // 4-7 bytes
+ (beg as *mut u32).write_unaligned(val as u32);
+ (end.sub(4) as *mut u32).write_unaligned(val as u32);
+ } else if remaining >= 2 {
+ // 2-3 bytes
+ (beg as *mut u16).write_unaligned(val as u16);
+ (end.sub(2) as *mut u16).write_unaligned(val as u16);
+ } else if remaining >= 1 {
+ // 1 byte
+ beg.write(val as u8);
+ }
+ }
+}
+
+#[cfg(target_arch = "aarch64")]
+unsafe fn memset_neon(mut beg: *mut u8, end: *mut u8, val: u64) {
+ unsafe {
+ use std::arch::aarch64::*;
+ let mut remaining = distance(end, beg);
+
+ if remaining >= 32 {
+ let fill = vdupq_n_u64(val);
+
+ loop {
+ // Compiles to a single `stp` instruction.
+ vst1q_u64(beg as *mut _, fill);
+ vst1q_u64(beg.add(16) as *mut _, fill);
+
+ beg = beg.add(32);
+ remaining -= 32;
+ if remaining < 32 {
+ break;
+ }
+ }
+ }
+
+ if remaining >= 16 {
+ // 16-31 bytes remaining
+ let fill = vdupq_n_u64(val);
+ vst1q_u64(beg as *mut _, fill);
+ vst1q_u64(end.sub(16) as *mut _, fill);
+ } else if remaining >= 8 {
+ // 8-15 bytes remaining
+ (beg as *mut u64).write_unaligned(val);
+ (end.sub(8) as *mut u64).write_unaligned(val);
+ } else if remaining >= 4 {
+ // 4-7 bytes remaining
+ (beg as *mut u32).write_unaligned(val as u32);
+ (end.sub(4) as *mut u32).write_unaligned(val as u32);
+ } else if remaining >= 2 {
+ // 2-3 bytes remaining
+ (beg as *mut u16).write_unaligned(val as u16);
+ (end.sub(2) as *mut u16).write_unaligned(val as u16);
+ } else if remaining >= 1 {
+ // 1 byte remaining
+ beg.write(val as u8);
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::fmt;
+ use std::ops::Not;
+
+ use super::*;
+
+ fn check_memset(val: T, len: usize)
+ where
+ T: MemsetSafe + Not