mirror of
https://github.com/microsoft/edit.git
synced 2025-07-07 21:35:16 +00:00
Merge remote-tracking branch 'ms/main' into uefi
This commit is contained in:
commit
1969391b52
43 changed files with 1354 additions and 584 deletions
|
@ -16,7 +16,7 @@ rustflags = [
|
|||
# = Huge reduction in binary size by removing all that.
|
||||
[unstable]
|
||||
build-std = ["std", "panic_abort"]
|
||||
build-std-features = ["panic_immediate_abort"]
|
||||
build-std-features = ["panic_immediate_abort", "optimize_for_size"]
|
||||
|
||||
# vvv The following parts are specific to official Windows builds. vvv
|
||||
# (The use of internal registries, security features, etc., are mandatory.)
|
||||
|
|
|
@ -20,7 +20,4 @@ rustflags = [
|
|||
# = Huge reduction in binary size by removing all that.
|
||||
[unstable]
|
||||
build-std = ["std", "panic_abort"]
|
||||
build-std-features = ["panic_immediate_abort"]
|
||||
|
||||
[build]
|
||||
target = "x86_64-unknown-uefi"
|
||||
build-std-features = ["panic_immediate_abort", "optimize_for_size"]
|
||||
|
|
48
.github/workflows/ci.yml
vendored
Normal file
48
.github/workflows/ci.yml
vendored
Normal file
|
@ -0,0 +1,48 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
steps:
|
||||
# The Windows runners have autocrlf enabled by default.
|
||||
- name: Disable git autocrlf
|
||||
run: git config --global core.autocrlf false
|
||||
if: matrix.os == 'windows-latest'
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
# https://github.com/actions/cache/blob/main/examples.md#rust---cargo
|
||||
# Depends on `Cargo.lock` --> Has to be after checkout.
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- name: Install Rust
|
||||
run: rustup toolchain install nightly --no-self-update --profile minimal --component rust-src,rustfmt,clippy
|
||||
- name: Check formatting
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Run tests
|
||||
run: cargo test --all-features --all-targets
|
||||
- name: Run clippy
|
||||
run: cargo clippy --all-features --all-targets -- --deny warnings
|
31
.github/workflows/winget.yml
vendored
Normal file
31
.github/workflows/winget.yml
vendored
Normal file
|
@ -0,0 +1,31 @@
|
|||
name: Submit release to the WinGet community repository
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
publish-winget:
|
||||
name: Submit to WinGet repository
|
||||
|
||||
# winget-create is only supported on Windows
|
||||
runs-on: windows-latest
|
||||
|
||||
# Only submit stable releases
|
||||
if: ${{ !github.event.release.prerelease }}
|
||||
steps:
|
||||
- name: Submit package using wingetcreate
|
||||
run: |
|
||||
# Get installer info from release event
|
||||
$assets = '${{ toJSON(github.event.release.assets) }}' | ConvertFrom-Json
|
||||
$x64InstallerUrl = $assets | Where-Object -Property name -like '*x86_64-windows.zip' | Select-Object -ExpandProperty browser_download_url
|
||||
$arm64InstallerUrl = $assets | Where-Object -Property name -like '*aarch64-windows.zip' | Select-Object -ExpandProperty browser_download_url
|
||||
$packageVersion = (${{ toJSON(github.event.release.tag_name) }}).Trim('v')
|
||||
|
||||
# Update package using wingetcreate
|
||||
curl.exe -JLO https://aka.ms/wingetcreate/latest
|
||||
.\wingetcreate.exe update Microsoft.Edit `
|
||||
--version $packageVersion `
|
||||
--urls $x64InstallerUrl $arm64InstallerUrl `
|
||||
--token "${{ secrets.WINGET_TOKEN }}" `
|
||||
--submit
|
|
@ -166,3 +166,19 @@ extends:
|
|||
search_root: "$(ob_createvpack_vpackdirectory)"
|
||||
use_testsign: false
|
||||
in_container: true
|
||||
|
||||
- ${{ each platform in parameters.buildPlatforms }}:
|
||||
- pwsh: |-
|
||||
$Dest = New-Item -Type Directory "_staging/${env:RELEASE_NAME}"
|
||||
Write-Host "Staging files from ${env:VPACK_ROOT} at $Dest"
|
||||
Get-ChildItem "${env:VPACK_ROOT}\*" -Include *.exe, *.pdb | Copy-Item -Destination $Dest -Verbose
|
||||
tar.exe -c -v --format=zip -f "$(ob_outputDirectory)\${env:RELEASE_NAME}.zip" -C _staging $env:RELEASE_NAME
|
||||
env:
|
||||
RELEASE_NAME: edit-$(EditVersion)-${{ replace(platform, 'pc-windows-msvc', 'windows') }}
|
||||
${{ if eq(platform, 'i686-pc-windows-msvc') }}:
|
||||
VPACK_ROOT: "$(ob_createvpack_vpackdirectory)/i386"
|
||||
${{ elseif eq(platform, 'x86_64-pc-windows-msvc') }}:
|
||||
VPACK_ROOT: "$(ob_createvpack_vpackdirectory)/amd64"
|
||||
${{ else }}: # aarch64-pc-windows-msvc
|
||||
VPACK_ROOT: "$(ob_createvpack_vpackdirectory)/arm64"
|
||||
displayName: Produce ${{platform}} release archive
|
||||
|
|
238
Cargo.lock
generated
238
Cargo.lock
generated
|
@ -37,9 +37,9 @@ checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
|
|||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.0"
|
||||
version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
|
||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
|
@ -53,6 +53,17 @@ version = "0.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
|
@ -88,18 +99,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.35"
|
||||
version = "4.5.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944"
|
||||
checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.35"
|
||||
version = "4.5.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9"
|
||||
checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
|
@ -113,25 +124,22 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
|||
|
||||
[[package]]
|
||||
name = "criterion"
|
||||
version = "0.5.1"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
|
||||
checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679"
|
||||
dependencies = [
|
||||
"anes",
|
||||
"cast",
|
||||
"ciborium",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"is-terminal",
|
||||
"itertools",
|
||||
"itertools 0.13.0",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"oorandom",
|
||||
"plotters",
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
"walkdir",
|
||||
|
@ -144,7 +152,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
|
||||
dependencies = [
|
||||
"cast",
|
||||
"itertools",
|
||||
"itertools 0.10.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -180,16 +188,19 @@ checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
|
|||
|
||||
[[package]]
|
||||
name = "edit"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"libc",
|
||||
"qemu-exit",
|
||||
"r-efi",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"uefi",
|
||||
"uefi-raw",
|
||||
"windows-sys",
|
||||
"winres",
|
||||
"winresource",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -198,6 +209,24 @@ version = "1.15.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.6.0"
|
||||
|
@ -209,20 +238,19 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.0"
|
||||
name = "hashbrown"
|
||||
version = "0.15.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e"
|
||||
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.16"
|
||||
name = "indexmap"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
|
||||
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -234,12 +262,31 @@ dependencies = [
|
|||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.77"
|
||||
|
@ -252,9 +299,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.171"
|
||||
version = "0.2.172"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
|
||||
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
|
@ -289,6 +336,12 @@ version = "11.1.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "plotters"
|
||||
version = "0.3.7"
|
||||
|
@ -319,9 +372,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.94"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
@ -418,9 +471,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
|||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.20"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
|
@ -470,10 +523,25 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.100"
|
||||
name = "serde_spanned"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
|
||||
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -492,13 +560,45 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.11"
|
||||
version = "0.8.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
|
||||
checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_write",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
|
||||
|
||||
[[package]]
|
||||
name = "ucs2"
|
||||
version = "0.3.3"
|
||||
|
@ -557,6 +657,12 @@ version = "1.0.18"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
|
@ -567,6 +673,15 @@ dependencies = [
|
|||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.14.2+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
|
||||
dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.100"
|
||||
|
@ -718,10 +833,57 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winres"
|
||||
version = "0.1.12"
|
||||
name = "winnow"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c"
|
||||
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winresource"
|
||||
version = "0.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a179ac8923651ff1d15efbee760b4dd3679fd85fa5a8b2bb1109b7248f80e30f"
|
||||
dependencies = [
|
||||
"toml",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "7.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.15+zstd.1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
|
10
Cargo.toml
10
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "edit"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.87"
|
||||
repository = "https://github.com/microsoft/edit"
|
||||
|
@ -12,7 +12,6 @@ name = "lib"
|
|||
harness = false
|
||||
|
||||
[features]
|
||||
debug-layout = []
|
||||
debug-latency = []
|
||||
|
||||
# We use `opt-level = "s"` as it significantly reduces binary size.
|
||||
|
@ -44,7 +43,7 @@ r-efi = { version = "5.2.0" }
|
|||
libc = "0.2"
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
winres = "0.1"
|
||||
winresource = "0.1.22"
|
||||
|
||||
[target.'cfg(windows)'.dependencies.windows-sys]
|
||||
version = "0.59"
|
||||
|
@ -61,4 +60,7 @@ features = [
|
|||
]
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
criterion = { version = "0.6", features = ["html_reports"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0" }
|
||||
zstd = { version = "0.13", default-features = false }
|
||||
|
|
5
assets/editing-traces/README.md
Normal file
5
assets/editing-traces/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# editing-traces
|
||||
|
||||
This directory contains Seph Gentle's ASCII-only `rustcode` editing traces from: https://github.com/josephg/editing-traces
|
||||
|
||||
The trace was provided under the [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) license.
|
BIN
assets/editing-traces/rustcode.json.zst
Normal file
BIN
assets/editing-traces/rustcode.json.zst
Normal file
Binary file not shown.
26
assets/manpage/edit.1
Normal file
26
assets/manpage/edit.1
Normal file
|
@ -0,0 +1,26 @@
|
|||
.TH EDIT 1 "version 1.0" "May 2025"
|
||||
.SH NAME
|
||||
edit \- a simple text editor
|
||||
.SH SYNOPSIS
|
||||
\fBedit\fP [\fIOPTIONS\fP]... [\fIARGUMENTS\fP]...
|
||||
.SH DESCRIPTION
|
||||
edit is a simple text editor inspired by MS-DOS edit.
|
||||
.SH EDITING
|
||||
Edit is an interactive mode-less editor. Use Alt-F to access the menus.
|
||||
.SH ARGUMENTS
|
||||
.TP
|
||||
\fIFILE[:LINE[:COLUMN]]\fP
|
||||
The file to open, optionally with line and column (e.g., \fBfoo.txt:123:45\fP).
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
\fB\-h\fP, \fB\-\-help\fP
|
||||
Print the help message.
|
||||
.TP
|
||||
\fB\-v\fP, \fB\-\-version\fP
|
||||
Print the version number.
|
||||
.SH COPYRIGHT
|
||||
Copyright (c) Microsoft Corporation.
|
||||
.br
|
||||
Licensed under the MIT License.
|
||||
.SH SEE ALSO
|
||||
https://github.com/microsoft/edit
|
102
benches/lib.rs
102
benches/lib.rs
|
@ -4,12 +4,109 @@
|
|||
#![feature(uefi_std)]
|
||||
|
||||
use std::hint::black_box;
|
||||
use std::io::Cursor;
|
||||
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};
|
||||
use edit::{arena, buffer, hash, oklab, simd, unicode};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EditingTracePatch(pub usize, pub usize, pub String);
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EditingTraceTransaction {
|
||||
pub patches: Vec<EditingTracePatch>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EditingTraceData {
|
||||
#[serde(rename = "startContent")]
|
||||
pub start_content: String,
|
||||
#[serde(rename = "endContent")]
|
||||
pub end_content: String,
|
||||
pub txns: Vec<EditingTraceTransaction>,
|
||||
}
|
||||
|
||||
fn bench_buffer(c: &mut Criterion) {
|
||||
let data = include_bytes!("../assets/editing-traces/rustcode.json.zst");
|
||||
let data = zstd::decode_all(Cursor::new(data)).unwrap();
|
||||
let data: EditingTraceData = serde_json::from_slice(&data).unwrap();
|
||||
let mut patches_with_coords = Vec::new();
|
||||
|
||||
{
|
||||
let mut tb = buffer::TextBuffer::new(false).unwrap();
|
||||
tb.set_crlf(false);
|
||||
tb.write(data.start_content.as_bytes(), true);
|
||||
|
||||
for t in &data.txns {
|
||||
for p in &t.patches {
|
||||
tb.cursor_move_to_offset(p.0);
|
||||
let beg = tb.cursor_logical_pos();
|
||||
|
||||
tb.delete(buffer::CursorMovement::Grapheme, p.1 as CoordType);
|
||||
|
||||
tb.write(p.2.as_bytes(), true);
|
||||
patches_with_coords.push((beg, p.1 as CoordType, p.2.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
let mut actual = String::new();
|
||||
tb.save_as_string(&mut actual);
|
||||
assert_eq!(actual, data.end_content);
|
||||
}
|
||||
|
||||
let bench_gap_buffer = || {
|
||||
let mut buf = buffer::GapBuffer::new(false).unwrap();
|
||||
buf.replace(0..usize::MAX, data.start_content.as_bytes());
|
||||
|
||||
for t in &data.txns {
|
||||
for p in &t.patches {
|
||||
buf.replace(p.0..p.0 + p.1, p.2.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
buf
|
||||
};
|
||||
|
||||
let bench_text_buffer = || {
|
||||
let mut tb = buffer::TextBuffer::new(false).unwrap();
|
||||
tb.set_crlf(false);
|
||||
tb.write(data.start_content.as_bytes(), true);
|
||||
|
||||
for p in &patches_with_coords {
|
||||
tb.cursor_move_to_logical(p.0);
|
||||
tb.delete(buffer::CursorMovement::Grapheme, p.1);
|
||||
tb.write(p.2.as_bytes(), true);
|
||||
}
|
||||
|
||||
tb
|
||||
};
|
||||
|
||||
// Sanity check: If this fails, the implementation is incorrect.
|
||||
{
|
||||
let buf = bench_gap_buffer();
|
||||
let mut actual = Vec::new();
|
||||
buf.extract_raw(0..usize::MAX, &mut actual, 0);
|
||||
assert_eq!(actual, data.end_content.as_bytes());
|
||||
}
|
||||
{
|
||||
let mut tb = bench_text_buffer();
|
||||
let mut actual = String::new();
|
||||
tb.save_as_string(&mut actual);
|
||||
assert_eq!(actual, data.end_content);
|
||||
}
|
||||
|
||||
c.benchmark_group("buffer")
|
||||
.bench_function(BenchmarkId::new("GapBuffer", "rustcode"), |b| {
|
||||
b.iter(bench_gap_buffer);
|
||||
})
|
||||
.bench_function(BenchmarkId::new("TextBuffer", "rustcode"), |b| {
|
||||
b.iter(bench_text_buffer);
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_hash(c: &mut Criterion) {
|
||||
c.benchmark_group("hash")
|
||||
|
@ -106,6 +203,9 @@ fn bench_unicode(c: &mut Criterion) {
|
|||
}
|
||||
|
||||
fn bench(c: &mut Criterion) {
|
||||
arena::init(128 * MEBI).unwrap();
|
||||
|
||||
bench_buffer(c);
|
||||
bench_hash(c);
|
||||
bench_oklab(c);
|
||||
bench_simd_memchr2(c);
|
||||
|
|
2
build.rs
2
build.rs
|
@ -4,7 +4,7 @@
|
|||
fn main() {
|
||||
#[cfg(windows)]
|
||||
if std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default() == "windows" {
|
||||
winres::WindowsResource::new()
|
||||
winresource::WindowsResource::new()
|
||||
.set_manifest_file("src/bin/edit/edit.exe.manifest")
|
||||
.set("FileDescription", "Microsoft Edit")
|
||||
.set("LegalCopyright", "© Microsoft Corporation. All rights reserved.")
|
||||
|
|
|
@ -23,15 +23,15 @@ pub enum Error {
|
|||
|
||||
impl Error {
|
||||
pub const fn new_app(code: u32) -> Self {
|
||||
Error::App(code)
|
||||
Self::App(code)
|
||||
}
|
||||
|
||||
pub const fn new_icu(code: u32) -> Self {
|
||||
Error::Icu(code)
|
||||
Self::Icu(code)
|
||||
}
|
||||
|
||||
pub const fn new_sys(code: u32) -> Self {
|
||||
Error::Sys(code)
|
||||
Self::Sys(code)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ pub enum Arena {
|
|||
|
||||
impl Drop for Arena {
|
||||
fn drop(&mut self) {
|
||||
if let Arena::Delegated { delegate, borrow } = self {
|
||||
if let Self::Delegated { delegate, borrow } = self {
|
||||
let borrows = delegate.borrows.get();
|
||||
assert_eq!(*borrow, borrows);
|
||||
delegate.borrows.set(borrows - 1);
|
||||
|
@ -63,11 +63,11 @@ impl Arena {
|
|||
Self::Owned { arena: release::Arena::empty() }
|
||||
}
|
||||
|
||||
pub fn new(capacity: usize) -> apperr::Result<Arena> {
|
||||
pub fn new(capacity: usize) -> apperr::Result<Self> {
|
||||
Ok(Self::Owned { arena: release::Arena::new(capacity)? })
|
||||
}
|
||||
|
||||
pub(super) fn delegated(delegate: &release::Arena) -> Arena {
|
||||
pub(super) fn delegated(delegate: &release::Arena) -> Self {
|
||||
let borrow = delegate.borrows.get() + 1;
|
||||
delegate.borrows.set(borrow);
|
||||
Self::Delegated { delegate: unsafe { mem::transmute(delegate) }, borrow }
|
||||
|
@ -76,22 +76,22 @@ impl Arena {
|
|||
#[inline]
|
||||
pub(super) fn delegate_target(&self) -> &release::Arena {
|
||||
match *self {
|
||||
Arena::Delegated { delegate, borrow } => {
|
||||
Self::Delegated { delegate, borrow } => {
|
||||
assert!(
|
||||
borrow == delegate.borrows.get(),
|
||||
"Arena already borrowed by a newer ScratchArena"
|
||||
);
|
||||
delegate
|
||||
}
|
||||
Arena::Owned { ref arena } => arena,
|
||||
Self::Owned { ref arena } => arena,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn delegate_target_unchecked(&self) -> &release::Arena {
|
||||
match self {
|
||||
Arena::Delegated { delegate, .. } => delegate,
|
||||
Arena::Owned { arena } => arena,
|
||||
Self::Delegated { delegate, .. } => delegate,
|
||||
Self::Owned { arena } => arena,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ const ALLOC_CHUNK_SIZE: usize = 64 * KIBI;
|
|||
///
|
||||
/// 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
|
||||
/// uses an arena to allocate a tree of UI nodes. This is infamously difficult
|
||||
/// to do in Rust, but not so when you got an arena allocator:
|
||||
/// All nodes have the same lifetime, so you can just use references.
|
||||
///
|
||||
|
@ -62,11 +62,11 @@ impl Arena {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn new(capacity: usize) -> apperr::Result<Arena> {
|
||||
pub fn new(capacity: usize) -> apperr::Result<Self> {
|
||||
let capacity = (capacity.max(1) + ALLOC_CHUNK_SIZE - 1) & !(ALLOC_CHUNK_SIZE - 1);
|
||||
let base = unsafe { sys::virtual_reserve(capacity)? };
|
||||
|
||||
Ok(Arena {
|
||||
Ok(Self {
|
||||
base,
|
||||
capacity,
|
||||
commit: Cell::new(0),
|
||||
|
@ -177,7 +177,7 @@ impl Drop for Arena {
|
|||
|
||||
impl Default for Arena {
|
||||
fn default() -> Self {
|
||||
Arena::empty()
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -50,10 +50,7 @@ impl<'a> ArenaString<'a> {
|
|||
/// 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>> {
|
||||
pub fn from_utf8_lossy<'s>(arena: &'a Arena, text: &'s [u8]) -> Result<&'s str, Self> {
|
||||
let mut iter = text.utf8_chunks();
|
||||
let Some(mut chunk) = iter.next() else {
|
||||
return Ok("");
|
||||
|
@ -133,7 +130,7 @@ impl<'a> ArenaString<'a> {
|
|||
}
|
||||
|
||||
/// Reserves *additional* memory. For you old folks out there (totally not me),
|
||||
/// this is differrent from C++'s `reserve` which reserves a total size.
|
||||
/// this is different from C++'s `reserve` which reserves a total size.
|
||||
pub fn reserve(&mut self, additional: usize) {
|
||||
self.vec.reserve(additional)
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ impl Document {
|
|||
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.dir = Some(DisplayablePathBuf::from_path(dir));
|
||||
self.path = Some(path);
|
||||
self.update_file_mode();
|
||||
}
|
||||
|
@ -114,13 +114,7 @@ impl DocumentManager {
|
|||
}
|
||||
|
||||
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 buffer = Self::create_buffer()?;
|
||||
let mut doc = Document {
|
||||
buffer,
|
||||
path: None,
|
||||
|
@ -167,13 +161,10 @@ impl DocumentManager {
|
|||
return Ok(doc);
|
||||
}
|
||||
|
||||
let buffer = TextBuffer::new_rc(false)?;
|
||||
let buffer = Self::create_buffer()?;
|
||||
{
|
||||
let mut tb = buffer.borrow_mut();
|
||||
tb.set_margin_enabled(true);
|
||||
tb.set_line_highlight_enabled(true);
|
||||
|
||||
if let Some(file) = &mut file {
|
||||
let mut tb = buffer.borrow_mut();
|
||||
tb.read_file(file, None)?;
|
||||
|
||||
if let Some(goto) = goto
|
||||
|
@ -194,6 +185,16 @@ impl DocumentManager {
|
|||
};
|
||||
doc.set_path(path);
|
||||
|
||||
if let Some(active) = self.active()
|
||||
&& active.path.is_none()
|
||||
&& active.file_id.is_none()
|
||||
&& !active.buffer.borrow().is_dirty()
|
||||
{
|
||||
// If the current document is a pristine Untitled document with no
|
||||
// name and no ID, replace it with the new document.
|
||||
self.remove_active();
|
||||
}
|
||||
|
||||
self.list.push_front(doc);
|
||||
Ok(self.list.front_mut().unwrap())
|
||||
}
|
||||
|
@ -206,6 +207,17 @@ impl DocumentManager {
|
|||
File::create(path).map_err(apperr::Error::from)
|
||||
}
|
||||
|
||||
fn create_buffer() -> apperr::Result<RcTextBuffer> {
|
||||
let buffer = TextBuffer::new_rc(false)?;
|
||||
{
|
||||
let mut tb = buffer.borrow_mut();
|
||||
tb.set_insert_final_newline(!cfg!(windows)); // As mandated by POSIX.
|
||||
tb.set_margin_enabled(true);
|
||||
tb.set_line_highlight_enabled(true);
|
||||
}
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
// Parse a filename in the form of "filename:line:char".
|
||||
// Returns the position of the first colon and the line/char coordinates.
|
||||
fn parse_filename_goto(path: &Path) -> (&Path, Option<Point>) {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use std::num::ParseIntError;
|
||||
|
||||
use edit::framebuffer::IndexedColor;
|
||||
use edit::helpers::*;
|
||||
use edit::icu;
|
||||
|
@ -123,9 +125,11 @@ fn draw_search(ctx: &mut Context, state: &mut State) {
|
|||
ctx.table_begin("options");
|
||||
ctx.table_set_cell_gap(Size { width: 2, height: 0 });
|
||||
{
|
||||
let mut change = false;
|
||||
let mut change_action = SearchAction::Search;
|
||||
|
||||
ctx.table_next_row();
|
||||
|
||||
let mut change = false;
|
||||
change |= ctx.checkbox(
|
||||
"match-case",
|
||||
loc(LocId::SearchMatchCase),
|
||||
|
@ -141,21 +145,21 @@ fn draw_search(ctx: &mut Context, state: &mut State) {
|
|||
loc(LocId::SearchUseRegex),
|
||||
&mut state.search_options.use_regex,
|
||||
);
|
||||
if state.wants_search.kind == StateSearchKind::Replace
|
||||
&& ctx.button("replace-all", loc(LocId::SearchReplaceAll), ButtonStyle::default())
|
||||
{
|
||||
change = true;
|
||||
change_action = SearchAction::ReplaceAll;
|
||||
}
|
||||
if ctx.button("close", loc(LocId::SearchClose), ButtonStyle::default()) {
|
||||
state.wants_search.kind = StateSearchKind::Hidden;
|
||||
}
|
||||
|
||||
if change {
|
||||
action = SearchAction::Search;
|
||||
action = change_action;
|
||||
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();
|
||||
}
|
||||
|
@ -202,7 +206,6 @@ pub fn draw_handle_save(ctx: &mut Context, state: &mut State) {
|
|||
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;
|
||||
};
|
||||
|
||||
|
@ -225,6 +228,8 @@ pub fn draw_handle_wants_close(ctx: &mut Context, state: &mut State) {
|
|||
ctx.attr_background_rgba(ctx.indexed(IndexedColor::Red));
|
||||
ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::BrightWhite));
|
||||
{
|
||||
let contains_focus = ctx.contains_focus();
|
||||
|
||||
ctx.label("description", loc(LocId::UnsavedChangesDialogDescription));
|
||||
ctx.attr_padding(Rect::three(1, 2, 1));
|
||||
|
||||
|
@ -237,22 +242,32 @@ pub fn draw_handle_wants_close(ctx: &mut Context, state: &mut State) {
|
|||
ctx.table_next_row();
|
||||
ctx.inherit_focus();
|
||||
|
||||
if ctx.button("yes", loc(LocId::UnsavedChangesDialogYes)) {
|
||||
if ctx.button(
|
||||
"yes",
|
||||
loc(LocId::UnsavedChangesDialogYes),
|
||||
ButtonStyle::default().accelerator('S'),
|
||||
) {
|
||||
action = Action::Save;
|
||||
}
|
||||
ctx.inherit_focus();
|
||||
if ctx.button("no", loc(LocId::UnsavedChangesDialogNo)) {
|
||||
if ctx.button(
|
||||
"no",
|
||||
loc(LocId::UnsavedChangesDialogNo),
|
||||
ButtonStyle::default().accelerator('N'),
|
||||
) {
|
||||
action = Action::Discard;
|
||||
}
|
||||
if ctx.button("cancel", loc(LocId::Cancel)) {
|
||||
if ctx.button("cancel", loc(LocId::Cancel), ButtonStyle::default()) {
|
||||
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;
|
||||
// Handle accelerator shortcuts
|
||||
if contains_focus {
|
||||
if ctx.consume_shortcut(vk::S) {
|
||||
action = Action::Save;
|
||||
} else if ctx.consume_shortcut(vk::N) {
|
||||
action = Action::Discard;
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.table_end();
|
||||
|
@ -271,3 +286,57 @@ pub fn draw_handle_wants_close(ctx: &mut Context, state: &mut State) {
|
|||
state.wants_close = false;
|
||||
ctx.toss_focus_up();
|
||||
}
|
||||
|
||||
pub fn draw_goto_menu(ctx: &mut Context, state: &mut State) {
|
||||
let mut done = false;
|
||||
|
||||
if let Some(doc) = state.documents.active_mut() {
|
||||
ctx.modal_begin("goto", loc(LocId::FileGoto));
|
||||
{
|
||||
if ctx.editline("goto-line", &mut state.goto_target) {
|
||||
state.goto_invalid = false;
|
||||
}
|
||||
if state.goto_invalid {
|
||||
ctx.attr_background_rgba(ctx.indexed(IndexedColor::Red));
|
||||
ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::BrightWhite));
|
||||
}
|
||||
|
||||
ctx.attr_intrinsic_size(Size { width: 24, height: 1 });
|
||||
ctx.steal_focus();
|
||||
|
||||
if ctx.consume_shortcut(vk::RETURN) {
|
||||
match validate_goto_point(&state.goto_target) {
|
||||
Ok(point) => {
|
||||
let mut buf = doc.buffer.borrow_mut();
|
||||
buf.cursor_move_to_logical(point);
|
||||
buf.make_cursor_visible();
|
||||
done = true;
|
||||
}
|
||||
Err(_) => state.goto_invalid = true,
|
||||
}
|
||||
ctx.needs_rerender();
|
||||
}
|
||||
}
|
||||
done |= ctx.modal_end();
|
||||
} else {
|
||||
done = true;
|
||||
}
|
||||
|
||||
if done {
|
||||
state.wants_goto = false;
|
||||
state.goto_target.clear();
|
||||
state.goto_invalid = false;
|
||||
ctx.needs_rerender();
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_goto_point(line: &str) -> Result<Point, ParseIntError> {
|
||||
let mut coords = [0; 2];
|
||||
let (y, x) = line.split_once(':').unwrap_or((line, "0"));
|
||||
// Using a loop here avoids 2 copies of the str->int code.
|
||||
// This makes the binary more compact.
|
||||
for (i, s) in [x, y].iter().enumerate() {
|
||||
coords[i] = s.parse::<CoordType>()?.saturating_sub(1);
|
||||
}
|
||||
Ok(Point { x: coords[0], y: coords[1] })
|
||||
}
|
||||
|
|
|
@ -3,13 +3,13 @@
|
|||
|
||||
use std::cmp::Ordering;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use edit::framebuffer::IndexedColor;
|
||||
use edit::helpers::*;
|
||||
use edit::input::vk;
|
||||
use edit::tui::*;
|
||||
use edit::{icu, path, sys};
|
||||
use edit::{icu, path};
|
||||
|
||||
use crate::localization::*;
|
||||
use crate::state::*;
|
||||
|
@ -83,14 +83,12 @@ pub fn draw_file_picker(ctx: &mut Context, state: &mut State) {
|
|||
},
|
||||
);
|
||||
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.next_block_id_mixin(state.file_picker_pending_dir_revision);
|
||||
{
|
||||
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())
|
||||
{
|
||||
match ctx.list_item(false, entry.as_str()) {
|
||||
ListSelection::Unchanged => {}
|
||||
ListSelection::Selected => {
|
||||
state.file_picker_pending_name = entry.as_path().into()
|
||||
|
@ -114,9 +112,7 @@ pub fn draw_file_picker(ctx: &mut Context, state: &mut 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(None, path).is_ok_and(|id| &id == file_id)
|
||||
&& path.exists()
|
||||
{
|
||||
state.file_picker_overwrite_warning = doit.take();
|
||||
}
|
||||
|
@ -133,6 +129,8 @@ pub fn draw_file_picker(ctx: &mut Context, state: &mut State) {
|
|||
ctx.attr_background_rgba(ctx.indexed(IndexedColor::Red));
|
||||
ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::BrightWhite));
|
||||
{
|
||||
let contains_focus = ctx.contains_focus();
|
||||
|
||||
ctx.label("description", loc(LocId::FileOverwriteWarningDescription));
|
||||
ctx.attr_overflow(Overflow::TruncateTail);
|
||||
ctx.attr_padding(Rect::three(1, 2, 1));
|
||||
|
@ -146,18 +144,20 @@ pub fn draw_file_picker(ctx: &mut Context, state: &mut State) {
|
|||
ctx.table_next_row();
|
||||
ctx.inherit_focus();
|
||||
|
||||
save = ctx.button("yes", loc(LocId::Yes));
|
||||
save = ctx.button("yes", loc(LocId::Yes), ButtonStyle::default());
|
||||
ctx.inherit_focus();
|
||||
|
||||
if ctx.button("no", loc(LocId::No)) {
|
||||
if ctx.button("no", loc(LocId::No), ButtonStyle::default()) {
|
||||
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 contains_focus {
|
||||
save |= ctx.consume_shortcut(vk::Y);
|
||||
if ctx.consume_shortcut(vk::N) {
|
||||
state.file_picker_overwrite_warning = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ctx.modal_end() {
|
||||
|
@ -196,19 +196,31 @@ pub fn draw_file_picker(ctx: &mut Context, state: &mut State) {
|
|||
|
||||
// Returns Some(path) if the path refers to a file.
|
||||
fn draw_file_picker_update_path(state: &mut State) -> Option<PathBuf> {
|
||||
let path = state.file_picker_pending_dir.as_path();
|
||||
let path = path.join(&state.file_picker_pending_name);
|
||||
let old_path = state.file_picker_pending_dir.as_path();
|
||||
let path = old_path.join(&state.file_picker_pending_name);
|
||||
let path = path::normalize(&path);
|
||||
|
||||
let (dir, name) = if path.is_dir() {
|
||||
(path.as_path(), PathBuf::new())
|
||||
// If the current path is C:\ and the user selects "..", we want to
|
||||
// navigate to the drive picker. Since `path::normalize` will turn C:\.. into C:\,
|
||||
// we can detect this by checking if the length of the path didn't change.
|
||||
let dir = if cfg!(windows)
|
||||
&& state.file_picker_pending_name == Path::new("..")
|
||||
// It's unnecessary to check the contents of the paths.
|
||||
&& old_path.as_os_str().len() == path.as_os_str().len()
|
||||
{
|
||||
Path::new("")
|
||||
} else {
|
||||
path.as_path()
|
||||
};
|
||||
(dir, PathBuf::new())
|
||||
} else {
|
||||
let dir = path.parent().unwrap_or(&path);
|
||||
let name = path.file_name().map_or(Default::default(), |s| s.into());
|
||||
(dir, name)
|
||||
};
|
||||
if dir != state.file_picker_pending_dir.as_path() {
|
||||
state.file_picker_pending_dir = DisplayablePathBuf::new(dir.to_path_buf());
|
||||
state.file_picker_pending_dir = DisplayablePathBuf::from_path(dir.to_path_buf());
|
||||
state.file_picker_entries = None;
|
||||
}
|
||||
|
||||
|
@ -219,9 +231,23 @@ fn draw_file_picker_update_path(state: &mut State) -> Option<PathBuf> {
|
|||
fn draw_dialog_saveas_refresh_files(state: &mut State) {
|
||||
let dir = state.file_picker_pending_dir.as_path();
|
||||
let mut files = Vec::new();
|
||||
let mut off = 0;
|
||||
|
||||
if dir.parent().is_some() {
|
||||
#[cfg(windows)]
|
||||
if dir.as_os_str().is_empty() {
|
||||
// If the path is empty, we are at the drive picker.
|
||||
// Add all drives as entries.
|
||||
for drive in edit::sys::drives() {
|
||||
files.push(DisplayablePathBuf::from_string(format!("{drive}:\\")));
|
||||
}
|
||||
|
||||
state.file_picker_entries = Some(files);
|
||||
return;
|
||||
}
|
||||
|
||||
if cfg!(windows) || dir.parent().is_some() {
|
||||
files.push(DisplayablePathBuf::from(".."));
|
||||
off = 1;
|
||||
}
|
||||
|
||||
if let Ok(iter) = fs::read_dir(dir) {
|
||||
|
@ -240,7 +266,6 @@ fn draw_dialog_saveas_refresh_files(state: &mut State) {
|
|||
}
|
||||
|
||||
// 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();
|
||||
|
|
|
@ -49,9 +49,9 @@ fn draw_menu_file(ctx: &mut Context, state: &mut State) {
|
|||
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::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;
|
||||
|
@ -91,6 +91,10 @@ fn draw_menu_edit(ctx: &mut Context, state: &mut State) {
|
|||
state.wants_search.focus = true;
|
||||
}
|
||||
}
|
||||
if ctx.menubar_menu_button(loc(LocId::EditSelectAll), 'A', kbmod::CTRL | vk::A) {
|
||||
tb.select_all();
|
||||
ctx.needs_rerender();
|
||||
}
|
||||
ctx.menubar_menu_end();
|
||||
}
|
||||
|
||||
|
@ -103,6 +107,12 @@ fn draw_menu_view(ctx: &mut Context, state: &mut State) {
|
|||
let mut tb = doc.buffer.borrow_mut();
|
||||
let word_wrap = tb.is_word_wrap_enabled();
|
||||
|
||||
if ctx.menubar_menu_button(loc(LocId::ViewDocumentPicker), 'P', kbmod::CTRL | vk::P) {
|
||||
state.wants_document_picker = true;
|
||||
}
|
||||
if ctx.menubar_menu_button(loc(LocId::FileGoto), 'G', kbmod::CTRL | vk::G) {
|
||||
state.wants_goto = true;
|
||||
}
|
||||
if ctx.menubar_menu_checkbox(loc(LocId::ViewWordWrap), 'W', kbmod::ALT | vk::Z, word_wrap) {
|
||||
tb.set_word_wrap(!word_wrap);
|
||||
ctx.needs_rerender();
|
||||
|
@ -151,7 +161,7 @@ pub fn draw_dialog_about(ctx: &mut Context, state: &mut State) {
|
|||
ctx.attr_padding(Rect::three(1, 2, 0));
|
||||
ctx.attr_position(Position::Center);
|
||||
{
|
||||
if ctx.button("ok", loc(LocId::Ok)) {
|
||||
if ctx.button("ok", loc(LocId::Ok), ButtonStyle::default()) {
|
||||
state.wants_about = false;
|
||||
}
|
||||
ctx.inherit_focus();
|
||||
|
|
|
@ -24,7 +24,7 @@ pub fn draw_statusbar(ctx: &mut Context, state: &mut State) {
|
|||
|
||||
ctx.table_next_row();
|
||||
|
||||
if ctx.button("newline", if tb.is_crlf() { "CRLF" } else { "LF" }) {
|
||||
if ctx.button("newline", if tb.is_crlf() { "CRLF" } else { "LF" }, ButtonStyle::default()) {
|
||||
let is_crlf = tb.is_crlf();
|
||||
tb.normalize_newlines(!is_crlf);
|
||||
}
|
||||
|
@ -33,7 +33,8 @@ pub fn draw_statusbar(ctx: &mut Context, state: &mut State) {
|
|||
ctx.steal_focus();
|
||||
}
|
||||
|
||||
state.wants_encoding_picker |= ctx.button("encoding", tb.encoding());
|
||||
state.wants_encoding_picker |=
|
||||
ctx.button("encoding", tb.encoding(), ButtonStyle::default());
|
||||
if state.wants_encoding_picker {
|
||||
if doc.path.is_some() {
|
||||
ctx.block_begin("frame");
|
||||
|
@ -47,11 +48,11 @@ pub fn draw_statusbar(ctx: &mut Context, state: &mut State) {
|
|||
ctx.attr_padding(Rect::two(0, 1));
|
||||
ctx.attr_border();
|
||||
{
|
||||
if ctx.button("reopen", loc(LocId::EncodingReopen)) {
|
||||
if ctx.button("reopen", loc(LocId::EncodingReopen), ButtonStyle::default()) {
|
||||
state.wants_encoding_change = StateEncodingChange::Reopen;
|
||||
}
|
||||
ctx.focus_on_first_present();
|
||||
if ctx.button("convert", loc(LocId::EncodingConvert)) {
|
||||
if ctx.button("convert", loc(LocId::EncodingConvert), ButtonStyle::default()) {
|
||||
state.wants_encoding_change = StateEncodingChange::Convert;
|
||||
}
|
||||
}
|
||||
|
@ -79,6 +80,7 @@ pub fn draw_statusbar(ctx: &mut Context, state: &mut State) {
|
|||
}),
|
||||
tb.tab_size(),
|
||||
),
|
||||
ButtonStyle::default(),
|
||||
);
|
||||
if state.wants_indentation_picker {
|
||||
ctx.table_begin("indentation-picker");
|
||||
|
@ -93,7 +95,7 @@ pub fn draw_statusbar(ctx: &mut Context, state: &mut State) {
|
|||
ctx.attr_padding(Rect::two(0, 1));
|
||||
ctx.table_set_cell_gap(Size { width: 1, height: 0 });
|
||||
{
|
||||
if ctx.consume_shortcut(vk::RETURN) {
|
||||
if ctx.contains_focus() && ctx.consume_shortcut(vk::RETURN) {
|
||||
ctx.toss_focus_up();
|
||||
}
|
||||
|
||||
|
@ -153,13 +155,13 @@ pub fn draw_statusbar(ctx: &mut Context, state: &mut State) {
|
|||
),
|
||||
);
|
||||
|
||||
#[cfg(any(feature = "debug-layout", feature = "debug-latency"))]
|
||||
#[cfg(feature = "debug-latency")]
|
||||
ctx.label(
|
||||
"stats",
|
||||
&arena_format!(ctx.arena(), "{}/{}", tb.logical_line_count(), tb.visual_line_count(),),
|
||||
);
|
||||
|
||||
if tb.is_overtype() && ctx.button("overtype", "OVR") {
|
||||
if tb.is_overtype() && ctx.button("overtype", "OVR", ButtonStyle::default()) {
|
||||
tb.set_overtype(false);
|
||||
ctx.needs_rerender();
|
||||
}
|
||||
|
@ -180,7 +182,7 @@ pub fn draw_statusbar(ctx: &mut Context, state: &mut State) {
|
|||
filename = &filename_buf;
|
||||
}
|
||||
|
||||
state.wants_document_picker |= ctx.button("filename", filename);
|
||||
state.wants_document_picker |= ctx.button("filename", filename, ButtonStyle::default());
|
||||
ctx.inherit_focus();
|
||||
ctx.attr_overflow(Overflow::TruncateMiddle);
|
||||
ctx.attr_position(Position::Right);
|
||||
|
|
|
@ -25,6 +25,7 @@ pub enum LocId {
|
|||
FileSaveAs,
|
||||
FileClose,
|
||||
FileExit,
|
||||
FileGoto,
|
||||
|
||||
// Edit menu
|
||||
Edit,
|
||||
|
@ -35,11 +36,13 @@ pub enum LocId {
|
|||
EditPaste,
|
||||
EditFind,
|
||||
EditReplace,
|
||||
EditSelectAll,
|
||||
|
||||
// View menu
|
||||
View,
|
||||
ViewFocusStatusbar,
|
||||
ViewWordWrap,
|
||||
ViewDocumentPicker,
|
||||
|
||||
// Help menu
|
||||
Help,
|
||||
|
@ -244,17 +247,17 @@ const S_LANG_LUT: [[&str; LangId::Count as usize]; LocId::Count as usize] = [
|
|||
],
|
||||
// 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 */ "新增檔案…",
|
||||
/* 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
|
||||
[
|
||||
|
@ -303,7 +306,7 @@ const S_LANG_LUT: [[&str; LangId::Count as usize]; LocId::Count as usize] = [
|
|||
/* en */ "Close Editor",
|
||||
/* de */ "Editor schließen",
|
||||
/* es */ "Cerrar editor",
|
||||
/* fr */ "Fermer l'éditeur",
|
||||
/* fr */ "Fermer l’éditeur",
|
||||
/* it */ "Chiudi editor",
|
||||
/* ja */ "エディターを閉じる",
|
||||
/* ko */ "편집기 닫기",
|
||||
|
@ -326,6 +329,20 @@ const S_LANG_LUT: [[&str; LangId::Count as usize]; LocId::Count as usize] = [
|
|||
/* zh_hans */ "退出",
|
||||
/* zh_hant */ "退出",
|
||||
],
|
||||
// FileGoto
|
||||
[
|
||||
/* en */ "Go to Line/Column…",
|
||||
/* de */ "Gehe zu Zeile/Spalte…",
|
||||
/* es */ "Ir a línea/columna…",
|
||||
/* fr */ "Aller à la ligne/colonne…",
|
||||
/* it */ "Vai a riga/colonna…",
|
||||
/* ja */ "行/列へ移動…",
|
||||
/* ko */ "행/열로 이동…",
|
||||
/* pt_br */ "Ir para linha/coluna…",
|
||||
/* ru */ "Перейти к строке/столбцу…",
|
||||
/* zh_hans */ "转到行/列…",
|
||||
/* zh_hant */ "跳至行/列…",
|
||||
],
|
||||
|
||||
// Edit (a menu bar item)
|
||||
[
|
||||
|
@ -439,6 +456,20 @@ const S_LANG_LUT: [[&str; LangId::Count as usize]; LocId::Count as usize] = [
|
|||
/* zh_hans */ "替换",
|
||||
/* zh_hant */ "取代",
|
||||
],
|
||||
// EditSelectAll
|
||||
[
|
||||
/* en */ "Select All",
|
||||
/* de */ "Alles auswählen",
|
||||
/* es */ "Seleccionar todo",
|
||||
/* fr */ "Tout sélectionner",
|
||||
/* it */ "Seleziona tutto",
|
||||
/* ja */ "すべて選択",
|
||||
/* ko */ "모두 선택",
|
||||
/* pt_br */ "Selecionar tudo",
|
||||
/* ru */ "Выделить всё",
|
||||
/* zh_hans */ "全选",
|
||||
/* zh_hant */ "全選"
|
||||
],
|
||||
|
||||
// View (a menu bar item)
|
||||
[
|
||||
|
@ -473,7 +504,7 @@ const S_LANG_LUT: [[&str; LangId::Count as usize]; LocId::Count as usize] = [
|
|||
/* en */ "Word Wrap",
|
||||
/* de */ "Zeilenumbruch",
|
||||
/* es */ "Ajuste de línea",
|
||||
/* fr */ "Retour à la ligne",
|
||||
/* fr */ "Retour automatique à la ligne",
|
||||
/* it */ "A capo automatico",
|
||||
/* ja */ "折り返し",
|
||||
/* ko */ "자동 줄 바꿈",
|
||||
|
@ -482,6 +513,20 @@ const S_LANG_LUT: [[&str; LangId::Count as usize]; LocId::Count as usize] = [
|
|||
/* zh_hans */ "自动换行",
|
||||
/* zh_hant */ "自動換行",
|
||||
],
|
||||
// ViewDocumentPicker
|
||||
[
|
||||
/* en */ "Document Picker…",
|
||||
/* de */ "Dokumentauswahl…",
|
||||
/* es */ "Selector de documentos…",
|
||||
/* fr */ "Sélecteur de documents…",
|
||||
/* it */ "Selettore di documenti…",
|
||||
/* ja */ "ドキュメントピッカー…",
|
||||
/* ko */ "문서 선택기…",
|
||||
/* pt_br */ "Seletor de documentos…",
|
||||
/* ru */ "Выбор документа…",
|
||||
/* zh_hans */ "文档选择器…",
|
||||
/* zh_hant */ "文件選擇器…",
|
||||
],
|
||||
|
||||
// Help (a menu bar item)
|
||||
[
|
||||
|
@ -547,7 +592,7 @@ const S_LANG_LUT: [[&str; LangId::Count as usize]; LocId::Count as usize] = [
|
|||
/* es */ "Guardar",
|
||||
/* fr */ "Enregistrer",
|
||||
/* it */ "Salva",
|
||||
/* ja */ "保存",
|
||||
/* ja */ "保存する",
|
||||
/* ko */ "저장",
|
||||
/* pt_br */ "Salvar",
|
||||
/* ru */ "Сохранить",
|
||||
|
@ -616,7 +661,7 @@ const S_LANG_LUT: [[&str; LangId::Count as usize]; LocId::Count as usize] = [
|
|||
// LargeClipboardWarningLine2
|
||||
[
|
||||
/* en */ "You copied {size} which may take a long time to share.",
|
||||
/* de */ "Sie haben {size} kopiert, das Weitergeben könnte lange dauern.",
|
||||
/* de */ "Sie haben {size} kopiert. Das Weitergeben könnte länger 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.",
|
||||
|
@ -632,7 +677,7 @@ const S_LANG_LUT: [[&str; LangId::Count as usize]; LocId::Count as usize] = [
|
|||
/* 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?",
|
||||
/* fr */ "Voulez-vous quand même l’envoyer ?",
|
||||
/* it */ "Vuoi inviarlo comunque?",
|
||||
/* ja */ "それでも送信しますか?",
|
||||
/* ko */ "그래도 전송하시겠습니까?",
|
||||
|
@ -733,9 +778,9 @@ const S_LANG_LUT: [[&str; LangId::Count as usize]; LocId::Count as usize] = [
|
|||
/* en */ "Match Case",
|
||||
/* de */ "Groß/Klein",
|
||||
/* es */ "May/Min",
|
||||
/* fr */ "Casse",
|
||||
/* fr */ "Resp. la casse",
|
||||
/* it */ "Maius/minus",
|
||||
/* ja */ "大/小文字",
|
||||
/* ja */ "大/小文字を区別",
|
||||
/* ko */ "대소문자",
|
||||
/* pt_br */ "Maius/minus",
|
||||
/* ru */ "Регистр",
|
||||
|
@ -749,7 +794,7 @@ const S_LANG_LUT: [[&str; LangId::Count as usize]; LocId::Count as usize] = [
|
|||
/* es */ "Palabra",
|
||||
/* fr */ "Mot entier",
|
||||
/* it */ "Parola",
|
||||
/* ja */ "単語単位",
|
||||
/* ja */ "単語全体",
|
||||
/* ko */ "전체 단어",
|
||||
/* pt_br */ "Palavra",
|
||||
/* ru */ "Слово",
|
||||
|
@ -801,31 +846,31 @@ const S_LANG_LUT: [[&str; LangId::Count as usize]; LocId::Count as usize] = [
|
|||
|
||||
// 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 */ "使用編碼重新打開",
|
||||
/* 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 */ "轉換為編碼",
|
||||
/* en */ "Convert to encoding…",
|
||||
/* de */ "In Kodierung konvertieren…",
|
||||
/* es */ "Convertir a otra codificación…",
|
||||
/* fr */ "Convertir vers l’encodage…",
|
||||
/* it */ "Converti in codifica…",
|
||||
/* ja */ "エンコーディングを変換…",
|
||||
/* ko */ "인코딩으로 변환…",
|
||||
/* pt_br */ "Converter para codificação…",
|
||||
/* ru */ "Преобразовать в кодировку…",
|
||||
/* zh_hans */ "转换为编码…",
|
||||
/* zh_hant */ "轉換為編碼…",
|
||||
],
|
||||
|
||||
// IndentationTabs
|
||||
|
@ -876,7 +921,7 @@ const S_LANG_LUT: [[&str; LangId::Count as usize]; LocId::Count as usize] = [
|
|||
/* en */ "File name:",
|
||||
/* de */ "Dateiname:",
|
||||
/* es */ "Nombre de archivo:",
|
||||
/* fr */ "Nom de fichier :",
|
||||
/* fr */ "Nom du fichier :",
|
||||
/* it */ "Nome del file:",
|
||||
/* ja */ "ファイル名:",
|
||||
/* ko */ "파일 이름:",
|
||||
|
@ -905,7 +950,7 @@ const S_LANG_LUT: [[&str; LangId::Count as usize]; LocId::Count as usize] = [
|
|||
/* 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?",
|
||||
/* fr */ "Le fichier existe déjà. Voulez-vous l’écraser ?",
|
||||
/* it */ "Il file esiste già. Vuoi sovrascriverlo?",
|
||||
/* ja */ "ファイルは既に存在します。上書きしますか?",
|
||||
/* ko */ "파일이 이미 존재합니다. 덮어쓰시겠습니까?",
|
||||
|
@ -919,6 +964,9 @@ const S_LANG_LUT: [[&str; LangId::Count as usize]; LocId::Count as usize] = [
|
|||
static mut S_LANG: LangId = LangId::en;
|
||||
|
||||
pub fn init() {
|
||||
// WARNING:
|
||||
// Generic language tags such as "zh" MUST be sorted after more specific tags such
|
||||
// as "zh-hant" to ensure that the prefix match finds the most specific one first.
|
||||
const LANG_MAP: &[(&str, LangId)] = &[
|
||||
("en", LangId::en),
|
||||
// ----------------
|
||||
|
@ -939,11 +987,11 @@ pub fn init() {
|
|||
let langs = sys::preferred_languages(&scratch);
|
||||
let mut lang = LangId::en;
|
||||
|
||||
for l in langs {
|
||||
'outer: for l in langs {
|
||||
for (prefix, id) in LANG_MAP {
|
||||
if l.starts_with_ignore_ascii_case(prefix) {
|
||||
lang = *id;
|
||||
break;
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
#![feature(let_chains, linked_list_cursors, os_string_truncate, string_from_utf8_lossy_owned)]
|
||||
#![feature(
|
||||
allocator_api,
|
||||
let_chains,
|
||||
linked_list_cursors,
|
||||
os_string_truncate,
|
||||
string_from_utf8_lossy_owned
|
||||
)]
|
||||
|
||||
mod documents;
|
||||
mod draw_editor;
|
||||
|
@ -14,7 +20,7 @@ mod state;
|
|||
use std::borrow::Cow;
|
||||
#[cfg(feature = "debug-latency")]
|
||||
use std::fmt::Write;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{env, process};
|
||||
use std::ffi::CString;
|
||||
|
||||
|
@ -22,7 +28,7 @@ use draw_editor::*;
|
|||
use draw_filepicker::*;
|
||||
use draw_menubar::*;
|
||||
use draw_statusbar::*;
|
||||
use edit::arena::{self, ArenaString, scratch_arena};
|
||||
use edit::arena::{self, Arena, ArenaString, scratch_arena};
|
||||
use edit::framebuffer::{self, IndexedColor};
|
||||
use edit::helpers::{KIBI, MEBI, MetricFormatter, Rect, Size};
|
||||
use edit::input::{self, kbmod, vk};
|
||||
|
@ -165,12 +171,6 @@ fn run() -> apperr::Result<()> {
|
|||
|
||||
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;
|
||||
|
@ -215,7 +215,7 @@ fn run() -> apperr::Result<()> {
|
|||
);
|
||||
|
||||
// "μs" is 3 bytes and 2 columns.
|
||||
let cols = status.len() as i32 - 3 + 2;
|
||||
let cols = status.len() as edit::helpers::CoordType - 3 + 2;
|
||||
|
||||
// Since the status may shrink and grow, we may have to overwrite the previous one with whitespace.
|
||||
let padding = (last_latency_width - cols).max(0);
|
||||
|
@ -252,11 +252,12 @@ fn run() -> apperr::Result<()> {
|
|||
|
||||
// Returns true if the application should exit early.
|
||||
fn handle_args(state: &mut State) -> apperr::Result<bool> {
|
||||
let scratch = scratch_arena(None);
|
||||
let mut paths: Vec<PathBuf, &Arena> = Vec::new_in(&*scratch);
|
||||
let mut cwd = env::current_dir()?;
|
||||
let mut path = None;
|
||||
|
||||
// The best CLI argument parser in the world.
|
||||
if let Some(arg) = env::args_os().nth(1) {
|
||||
for arg in env::args_os().skip(1) {
|
||||
if arg == "-h" || arg == "--help" || (cfg!(windows) && arg == "/?") {
|
||||
print_help();
|
||||
return Ok(true);
|
||||
|
@ -264,15 +265,21 @@ fn handle_args(state: &mut State) -> apperr::Result<bool> {
|
|||
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);
|
||||
paths.clear();
|
||||
break;
|
||||
}
|
||||
let p = cwd.join(Path::new(&arg));
|
||||
let p = path::normalize(&p);
|
||||
if !p.is_dir() {
|
||||
paths.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
for p in &paths {
|
||||
state.documents.add_file_path(p)?;
|
||||
}
|
||||
if let Some(parent) = paths.first().and_then(|p| p.parent()) {
|
||||
cwd = parent.to_path_buf();
|
||||
}
|
||||
|
||||
if let Some(mut file) = sys::open_stdin_if_redirected() {
|
||||
|
@ -280,13 +287,13 @@ fn handle_args(state: &mut State) -> apperr::Result<bool> {
|
|||
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 {
|
||||
} else if paths.is_empty() {
|
||||
// No files were passed, and stdin is not redirected.
|
||||
state.documents.add_untitled()?;
|
||||
}
|
||||
|
||||
state.file_picker_pending_dir = DisplayablePathBuf::new(cwd);
|
||||
state.file_picker_pending_dir = DisplayablePathBuf::from_path(cwd);
|
||||
state.file_picker_pending_dir_revision = state.file_picker_pending_dir_revision.wrapping_add(1);
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
|
@ -317,6 +324,9 @@ fn draw(ctx: &mut Context, state: &mut State) {
|
|||
if state.wants_exit {
|
||||
draw_handle_wants_exit(ctx, state);
|
||||
}
|
||||
if state.wants_goto {
|
||||
draw_goto_menu(ctx, state);
|
||||
}
|
||||
if state.wants_file_picker != StateFilePicker::None {
|
||||
draw_file_picker(ctx, state);
|
||||
}
|
||||
|
@ -356,6 +366,8 @@ fn draw(ctx: &mut Context, state: &mut State) {
|
|||
state.wants_document_picker = true;
|
||||
} else if key == kbmod::CTRL | vk::Q {
|
||||
state.wants_exit = true;
|
||||
} else if key == kbmod::CTRL | vk::G {
|
||||
state.wants_goto = true;
|
||||
} else if key == kbmod::CTRL | vk::F && state.wants_search.kind != StateSearchKind::Disabled
|
||||
{
|
||||
state.wants_search.kind = StateSearchKind::Search;
|
||||
|
@ -454,18 +466,18 @@ fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) {
|
|||
ctx.inherit_focus();
|
||||
|
||||
if over_limit {
|
||||
if ctx.button("ok", loc(LocId::Ok)) {
|
||||
if ctx.button("ok", loc(LocId::Ok), ButtonStyle::default()) {
|
||||
state.osc_clipboard_seen_generation = generation;
|
||||
}
|
||||
ctx.inherit_focus();
|
||||
} else {
|
||||
if ctx.button("always", loc(LocId::Always)) {
|
||||
if ctx.button("always", loc(LocId::Always), ButtonStyle::default()) {
|
||||
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)) {
|
||||
if ctx.button("yes", loc(LocId::Yes), ButtonStyle::default()) {
|
||||
state.osc_clipboard_seen_generation = generation;
|
||||
state.osc_clipboard_send_generation = generation;
|
||||
}
|
||||
|
@ -473,7 +485,7 @@ fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) {
|
|||
ctx.inherit_focus();
|
||||
}
|
||||
|
||||
if ctx.button("no", loc(LocId::No)) {
|
||||
if ctx.button("no", loc(LocId::No), ButtonStyle::default()) {
|
||||
state.osc_clipboard_seen_generation = generation;
|
||||
}
|
||||
if ctx.clipboard().len() >= 10 * LARGE_CLIPBOARD_THRESHOLD {
|
||||
|
@ -510,7 +522,8 @@ 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[?1036l\x1b[?1002;1006;2004l\x1b[?1049l");
|
||||
// We specifically don't reset mode 1036, because most applications expect it to be set nowadays.
|
||||
sys::write_stdout("\x1b[0 q\x1b[?25h\x1b]0;\x07\x1b[?1002;1006;2004l\x1b[?1049l");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -614,7 +627,7 @@ fn setup_terminal(tui: &mut Tui, vt_parser: &mut vt::Parser) -> RestoreModes {
|
|||
RestoreModes
|
||||
}
|
||||
|
||||
/// Strips all C0 control characters from the string an replaces them with "_".
|
||||
/// Strips all C0 control characters from the string and replaces them with "_".
|
||||
///
|
||||
/// Jury is still out on whether this should also strip C1 control characters.
|
||||
/// That requires parsing UTF8 codepoints, which is annoying.
|
||||
|
|
|
@ -6,7 +6,6 @@ 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::*;
|
||||
|
@ -20,7 +19,7 @@ pub struct FormatApperr(apperr::Error);
|
|||
|
||||
impl From<apperr::Error> for FormatApperr {
|
||||
fn from(err: apperr::Error) -> Self {
|
||||
FormatApperr(err)
|
||||
Self(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,7 +40,13 @@ pub struct DisplayablePathBuf {
|
|||
}
|
||||
|
||||
impl DisplayablePathBuf {
|
||||
pub fn new(value: PathBuf) -> Self {
|
||||
#[allow(dead_code, reason = "only used on Windows")]
|
||||
pub fn from_string(str: String) -> Self {
|
||||
let value = PathBuf::from(&str);
|
||||
Self { value, str: Cow::Owned(str) }
|
||||
}
|
||||
|
||||
pub fn from_path(value: PathBuf) -> Self {
|
||||
let str = value.to_string_lossy();
|
||||
let str = unsafe { mem::transmute::<Cow<'_, str>, Cow<'_, str>>(str) };
|
||||
Self { value, str }
|
||||
|
@ -68,19 +73,19 @@ impl Default for DisplayablePathBuf {
|
|||
|
||||
impl Clone for DisplayablePathBuf {
|
||||
fn clone(&self) -> Self {
|
||||
DisplayablePathBuf::new(self.value.clone())
|
||||
Self::from_path(self.value.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OsString> for DisplayablePathBuf {
|
||||
fn from(s: OsString) -> DisplayablePathBuf {
|
||||
DisplayablePathBuf::new(PathBuf::from(s))
|
||||
fn from(s: OsString) -> Self {
|
||||
Self::from_path(PathBuf::from(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized + AsRef<OsStr>> From<&T> for DisplayablePathBuf {
|
||||
fn from(s: &T) -> DisplayablePathBuf {
|
||||
DisplayablePathBuf::new(PathBuf::from(s))
|
||||
fn from(s: &T) -> Self {
|
||||
Self::from_path(PathBuf::from(s))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -126,6 +131,7 @@ pub struct State {
|
|||
|
||||
pub wants_file_picker: StateFilePicker,
|
||||
pub file_picker_pending_dir: DisplayablePathBuf,
|
||||
pub file_picker_pending_dir_revision: u64, // Bumped every time `file_picker_pending_dir` changes.
|
||||
pub file_picker_pending_name: PathBuf,
|
||||
pub file_picker_entries: Option<Vec<DisplayablePathBuf>>,
|
||||
pub file_picker_overwrite_warning: Option<PathBuf>, // The path the warning is about.
|
||||
|
@ -145,6 +151,9 @@ pub struct State {
|
|||
pub wants_about: bool,
|
||||
pub wants_close: bool,
|
||||
pub wants_exit: bool,
|
||||
pub wants_goto: bool,
|
||||
pub goto_target: String,
|
||||
pub goto_invalid: bool,
|
||||
|
||||
pub osc_title_filename: String,
|
||||
pub osc_clipboard_seen_generation: u32,
|
||||
|
@ -155,13 +164,6 @@ pub struct State {
|
|||
|
||||
impl State {
|
||||
pub fn new() -> apperr::Result<Self> {
|
||||
let buffer = TextBuffer::new_rc(false)?;
|
||||
{
|
||||
let mut tb = buffer.borrow_mut();
|
||||
tb.set_margin_enabled(true);
|
||||
tb.set_line_highlight_enabled(true);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
menubar_color_bg: 0,
|
||||
menubar_color_fg: 0,
|
||||
|
@ -174,6 +176,7 @@ impl State {
|
|||
|
||||
wants_file_picker: StateFilePicker::None,
|
||||
file_picker_pending_dir: Default::default(),
|
||||
file_picker_pending_dir_revision: 0,
|
||||
file_picker_pending_name: Default::default(),
|
||||
file_picker_entries: None,
|
||||
file_picker_overwrite_warning: None,
|
||||
|
@ -193,6 +196,9 @@ impl State {
|
|||
wants_about: false,
|
||||
wants_close: false,
|
||||
wants_exit: false,
|
||||
wants_goto: false,
|
||||
goto_target: Default::default(),
|
||||
goto_invalid: false,
|
||||
|
||||
osc_title_filename: Default::default(),
|
||||
osc_clipboard_seen_generation: 0,
|
||||
|
@ -242,7 +248,7 @@ pub fn draw_error_log(ctx: &mut Context, state: &mut State) {
|
|||
}
|
||||
ctx.block_end();
|
||||
|
||||
if ctx.button("ok", loc(LocId::Ok)) {
|
||||
if ctx.button("ok", loc(LocId::Ok), ButtonStyle::default()) {
|
||||
state.error_log_count = 0;
|
||||
}
|
||||
ctx.attr_position(Position::Center);
|
||||
|
|
|
@ -30,7 +30,7 @@ enum BackingBuffer {
|
|||
impl Drop for BackingBuffer {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
if let BackingBuffer::VirtualMemory(ptr, reserve) = *self {
|
||||
if let Self::VirtualMemory(ptr, reserve) = *self {
|
||||
sys::virtual_release(ptr, reserve);
|
||||
}
|
||||
}
|
||||
|
@ -245,17 +245,9 @@ impl GapBuffer {
|
|||
self.text_length = 0;
|
||||
}
|
||||
|
||||
pub fn extract_raw(
|
||||
&self,
|
||||
mut beg: usize,
|
||||
mut end: usize,
|
||||
out: &mut Vec<u8>,
|
||||
mut out_off: usize,
|
||||
) {
|
||||
debug_assert!(beg <= end && end <= self.text_length);
|
||||
|
||||
end = end.min(self.text_length);
|
||||
beg = beg.min(end);
|
||||
pub fn extract_raw(&self, range: Range<usize>, out: &mut Vec<u8>, mut out_off: usize) {
|
||||
let end = range.end.min(self.text_length);
|
||||
let mut beg = range.start.min(end);
|
||||
out_off = out_off.min(out.len());
|
||||
|
||||
if beg >= end {
|
||||
|
|
|
@ -34,7 +34,7 @@ use std::ops::Range;
|
|||
use std::rc::Rc;
|
||||
use std::str;
|
||||
|
||||
use gap_buffer::GapBuffer;
|
||||
pub use gap_buffer::GapBuffer;
|
||||
|
||||
use crate::arena::{ArenaString, scratch_arena};
|
||||
use crate::cell::SemiRefCell;
|
||||
|
@ -204,6 +204,7 @@ pub struct TextBuffer {
|
|||
ruler: CoordType,
|
||||
encoding: &'static str,
|
||||
newlines_are_crlf: bool,
|
||||
insert_final_newline: bool,
|
||||
overtype: bool,
|
||||
|
||||
wants_cursor_visibility: bool,
|
||||
|
@ -213,7 +214,7 @@ impl TextBuffer {
|
|||
/// Creates a new text buffer inside an [`Rc`].
|
||||
/// See [`TextBuffer::new()`].
|
||||
pub fn new_rc(small: bool) -> apperr::Result<RcTextBuffer> {
|
||||
let buffer = TextBuffer::new(small)?;
|
||||
let buffer = Self::new(small)?;
|
||||
Ok(Rc::new(SemiRefCell::new(buffer)))
|
||||
}
|
||||
|
||||
|
@ -249,7 +250,8 @@ impl TextBuffer {
|
|||
line_highlight_enabled: false,
|
||||
ruler: 0,
|
||||
encoding: "UTF-8",
|
||||
newlines_are_crlf: cfg!(windows), // Unfortunately Windows users insist on CRLF
|
||||
newlines_are_crlf: cfg!(windows), // Windows users want CRLF
|
||||
insert_final_newline: false,
|
||||
overtype: false,
|
||||
|
||||
wants_cursor_visibility: false,
|
||||
|
@ -312,6 +314,11 @@ impl TextBuffer {
|
|||
self.newlines_are_crlf
|
||||
}
|
||||
|
||||
/// Changes the newline type without normalizing the document.
|
||||
pub fn set_crlf(&mut self, crlf: bool) {
|
||||
self.newlines_are_crlf = crlf;
|
||||
}
|
||||
|
||||
/// Changes the newline type used in the document.
|
||||
///
|
||||
/// NOTE: Cannot be undone.
|
||||
|
@ -381,6 +388,12 @@ impl TextBuffer {
|
|||
self.newlines_are_crlf = crlf;
|
||||
}
|
||||
|
||||
/// If enabled, automatically insert a final newline
|
||||
/// when typing at the end of the file.
|
||||
pub fn set_insert_final_newline(&mut self, enabled: bool) {
|
||||
self.insert_final_newline = enabled;
|
||||
}
|
||||
|
||||
/// Whether to insert or overtype text when writing.
|
||||
pub fn is_overtype(&self) -> bool {
|
||||
self.overtype
|
||||
|
@ -621,6 +634,7 @@ impl TextBuffer {
|
|||
// * the logical line count
|
||||
// * the newline type (LF or CRLF)
|
||||
// * the indentation type (tabs or spaces)
|
||||
// * whether there's a final newline
|
||||
{
|
||||
let chunk = self.read_forward(0);
|
||||
let mut offset = 0;
|
||||
|
@ -711,10 +725,13 @@ impl TextBuffer {
|
|||
(_, lines) = unicode::newlines_forward(chunk, offset, lines, CoordType::MAX);
|
||||
}
|
||||
|
||||
let final_newline = chunk.ends_with(b"\n");
|
||||
|
||||
// Add 1, because the last line doesn't end in a newline (it ends in the literal end).
|
||||
self.stats.logical_lines = lines + 1;
|
||||
self.stats.visual_lines = self.stats.logical_lines;
|
||||
self.newlines_are_crlf = newlines_are_crlf;
|
||||
self.insert_final_newline = final_newline;
|
||||
self.indent_with_tabs = indent_with_tabs;
|
||||
self.tab_size = tab_size;
|
||||
}
|
||||
|
@ -903,11 +920,16 @@ impl TextBuffer {
|
|||
}
|
||||
|
||||
fn set_selection(&mut self, selection: Option<TextBufferSelection>) -> u32 {
|
||||
self.selection = selection;
|
||||
self.selection = selection.filter(|s| s.beg != s.end);
|
||||
self.selection_generation = self.selection_generation.wrapping_add(1);
|
||||
self.selection_generation
|
||||
}
|
||||
|
||||
/// Moves the cursor by `offset` and updates the selection to contain it.
|
||||
pub fn selection_update_offset(&mut self, offset: usize) {
|
||||
self.set_cursor_for_selection(self.cursor_move_to_offset_internal(self.cursor, offset));
|
||||
}
|
||||
|
||||
/// Moves the cursor to `visual_pos` and updates the selection to contain it.
|
||||
pub fn selection_update_visual(&mut self, visual_pos: Point) {
|
||||
self.set_cursor_for_selection(self.cursor_move_to_visual_internal(self.cursor, visual_pos));
|
||||
|
@ -1083,6 +1105,10 @@ impl TextBuffer {
|
|||
pattern: &str,
|
||||
options: SearchOptions,
|
||||
) -> apperr::Result<ActiveSearch> {
|
||||
if pattern.is_empty() {
|
||||
return Err(apperr::Error::Icu(1)); // U_ILLEGAL_ARGUMENT_ERROR
|
||||
}
|
||||
|
||||
let sanitized_pattern = if options.whole_word && options.use_regex {
|
||||
Cow::Owned(format!(r"\b(?:{pattern})\b"))
|
||||
} else if options.whole_word {
|
||||
|
@ -1136,11 +1162,10 @@ impl TextBuffer {
|
|||
|
||||
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) };
|
||||
unsafe { search.regex.set_text(&mut search.text, offset) };
|
||||
search.buffer_generation = self.buffer.generation();
|
||||
}
|
||||
|
||||
if search.next_search_offset != offset {
|
||||
search.next_search_offset = offset;
|
||||
} else if search.next_search_offset != offset {
|
||||
search.next_search_offset = offset;
|
||||
search.regex.reset(offset);
|
||||
}
|
||||
|
@ -1241,7 +1266,7 @@ impl TextBuffer {
|
|||
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
|
||||
// can map to two visual positions: One at the end of the preceding line in front of
|
||||
// a word wrap, and another at the start of the next line after the same word wrap.
|
||||
//
|
||||
// This, however, only applies if we go upwards, because only then `bottom ≅ cursor`,
|
||||
|
@ -1322,11 +1347,11 @@ impl TextBuffer {
|
|||
|
||||
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 {
|
||||
if pos.y != cursor.visual_pos.y || pos.x < cursor.visual_pos.x {
|
||||
cursor = self.goto_line_start(cursor, pos.y);
|
||||
}
|
||||
} else {
|
||||
// `goto_visual()` can only seek foward, so we need to seek backward here if needed.
|
||||
// `goto_visual()` can only seek forward, so we need to seek backward here if needed.
|
||||
// NOTE that this intentionally doesn't use the `Eq` trait of `Point`, because if
|
||||
// `pos.y == cursor.visual_pos.y` we don't need to go to `cursor.logical_pos.y - 1`.
|
||||
while pos.y < cursor.visual_pos.y {
|
||||
|
@ -1895,6 +1920,22 @@ impl TextBuffer {
|
|||
}
|
||||
}
|
||||
|
||||
// POSIX mandates that all valid lines end in a newline.
|
||||
// This isn't all that common on Windows and so we have
|
||||
// `self.final_newline` to control this.
|
||||
//
|
||||
// In order to not annoy people with this, we only add a
|
||||
// newline if you just edited the very end of the buffer.
|
||||
if self.insert_final_newline
|
||||
&& self.cursor.offset > 0
|
||||
&& self.cursor.offset == self.text_length()
|
||||
&& self.cursor.logical_pos.x > 0
|
||||
{
|
||||
let cursor = self.cursor;
|
||||
self.edit_write(if self.newlines_are_crlf { b"\r\n" } else { b"\n" });
|
||||
self.set_cursor_internal(cursor);
|
||||
}
|
||||
|
||||
self.edit_end();
|
||||
}
|
||||
|
||||
|
@ -1904,7 +1945,9 @@ impl TextBuffer {
|
|||
/// 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);
|
||||
if delta == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut beg;
|
||||
let mut end;
|
||||
|
@ -1912,8 +1955,8 @@ impl TextBuffer {
|
|||
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())
|
||||
if (delta < 0 && self.cursor.offset == 0)
|
||||
|| (delta > 0 && self.cursor.offset >= self.text_length())
|
||||
{
|
||||
// Nothing to delete.
|
||||
return;
|
||||
|
@ -1983,7 +2026,7 @@ impl TextBuffer {
|
|||
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);
|
||||
self.buffer.extract_raw(beg.offset..end.offset, &mut replacement, 0);
|
||||
|
||||
let initial_len = replacement.len();
|
||||
let mut offset = 0;
|
||||
|
@ -2047,7 +2090,7 @@ impl TextBuffer {
|
|||
};
|
||||
|
||||
let mut out = Vec::new();
|
||||
self.buffer.extract_raw(beg.offset, end.offset, &mut out, 0);
|
||||
self.buffer.extract_raw(beg.offset..end.offset, &mut out, 0);
|
||||
|
||||
if delete && !out.is_empty() {
|
||||
self.edit_begin(HistoryType::Delete, beg);
|
||||
|
@ -2194,7 +2237,7 @@ impl TextBuffer {
|
|||
|
||||
// Copy the deleted portion into the undo entry.
|
||||
let deleted = &mut undo.deleted;
|
||||
self.buffer.extract_raw(off, to.offset, deleted, out_off);
|
||||
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;
|
||||
|
|
|
@ -13,7 +13,7 @@ enum CharClass {
|
|||
Word,
|
||||
}
|
||||
|
||||
const fn construct_classifier(seperators: &[u8]) -> [CharClass; 256] {
|
||||
const fn construct_classifier(separators: &[u8]) -> [CharClass; 256] {
|
||||
let mut classifier = [CharClass::Word; 256];
|
||||
|
||||
classifier[b' ' as usize] = CharClass::Whitespace;
|
||||
|
@ -22,9 +22,9 @@ const fn construct_classifier(seperators: &[u8]) -> [CharClass; 256] {
|
|||
classifier[b'\r' as usize] = CharClass::Newline;
|
||||
|
||||
let mut i = 0;
|
||||
let len = seperators.len();
|
||||
let len = separators.len();
|
||||
while i < len {
|
||||
let ch = seperators[i];
|
||||
let ch = separators[i];
|
||||
assert!(ch < 128, "Only ASCII separators are supported.");
|
||||
classifier[ch as usize] = CharClass::Separator;
|
||||
i += 1;
|
||||
|
@ -58,7 +58,7 @@ fn word_navigation<T: WordNavigation>(mut nav: T) -> usize {
|
|||
// Skip any whitespace.
|
||||
nav.skip_class(CharClass::Whitespace);
|
||||
|
||||
// Skip one word or seperator and take note of the class.
|
||||
// Skip one word or separator and take note of the class.
|
||||
let class = nav.peek(CharClass::Whitespace);
|
||||
if matches!(class, CharClass::Separator | CharClass::Word) {
|
||||
nav.next();
|
||||
|
|
|
@ -49,7 +49,7 @@ mod release {
|
|||
|
||||
impl<'b, T> Ref<'b, T> {
|
||||
#[inline(always)]
|
||||
pub fn clone(orig: &Ref<'b, T>) -> Ref<'b, T> {
|
||||
pub fn clone(orig: &Self) -> Self {
|
||||
Ref(orig.0)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,6 +104,6 @@ impl WriteableDocument for PathBuf {
|
|||
fn replace(&mut self, range: Range<usize>, replacement: &[u8]) {
|
||||
let mut vec = mem::take(self).into_os_string().into_encoded_bytes();
|
||||
vec.replace_range(range, replacement);
|
||||
*self = unsafe { PathBuf::from(OsString::from_encoded_bytes_unchecked(vec)) };
|
||||
*self = unsafe { Self::from(OsString::from_encoded_bytes_unchecked(vec)) };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -854,7 +854,7 @@ impl LineBuffer {
|
|||
|
||||
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.
|
||||
// we stopped before the wide glyph and thus must step forward to the next glyph.
|
||||
cursor = cfg.goto_logical(Point { x: cursor.logical_pos.x + 1, y: 0 });
|
||||
}
|
||||
|
||||
|
@ -1026,12 +1026,12 @@ 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 None: Self = Self(0);
|
||||
pub const Italic: Self = Self(0b1);
|
||||
pub const Underlined: Self = Self(0b10);
|
||||
pub const All: Self = Self(0b11);
|
||||
|
||||
pub const fn is(self, attr: Attributes) -> bool {
|
||||
pub const fn is(self, attr: Self) -> bool {
|
||||
(self.0 & attr.0) == attr.0
|
||||
}
|
||||
}
|
||||
|
@ -1039,18 +1039,18 @@ impl Attributes {
|
|||
unsafe impl MemsetSafe for Attributes {}
|
||||
|
||||
impl BitOr for Attributes {
|
||||
type Output = Attributes;
|
||||
type Output = Self;
|
||||
|
||||
fn bitor(self, rhs: Self) -> Self::Output {
|
||||
Attributes(self.0 | rhs.0)
|
||||
Self(self.0 | rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl BitXor for Attributes {
|
||||
type Output = Attributes;
|
||||
type Output = Self;
|
||||
|
||||
fn bitxor(self, rhs: Self) -> Self::Output {
|
||||
Attributes(self.0 ^ rhs.0)
|
||||
Self(self.0 ^ rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ pub fn score_fuzzy<'a>(
|
|||
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
|
||||
// If we are not matching on the first query character anymore, we only produce a
|
||||
// score if we had a score previously for the last query index (by looking at the diagScore).
|
||||
// This makes sure that the query always matches in sequence on the target. For example
|
||||
// given a target of "ede" and a query of "de", we would otherwise produce a wrong high score
|
||||
|
@ -101,7 +101,7 @@ pub fn score_fuzzy<'a>(
|
|||
// 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
|
||||
// Looking at an index above 0 in the query means we must have already
|
||||
// found out this is contiguous otherwise there wouldn't have been a score
|
||||
query_index > 0 ||
|
||||
// lastly check if the query is completely contiguous at this index in the target
|
||||
|
|
|
@ -41,14 +41,14 @@ impl fmt::Display for MetricFormatter<usize> {
|
|||
}
|
||||
|
||||
/// A viewport coordinate type used throughout the application.
|
||||
pub type CoordType = i32;
|
||||
pub type CoordType = isize;
|
||||
|
||||
/// 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;
|
||||
/// To avoid overflow issues because you're adding two [`CoordType::MAX`]
|
||||
/// values together, you can use [`COORD_TYPE_SAFE_MAX`] instead.
|
||||
///
|
||||
/// It equates to half the bits contained in [`CoordType`], which
|
||||
/// for instance is 32767 (0x7FFF) when [`CoordType`] is a [`i32`].
|
||||
pub const COORD_TYPE_SAFE_MAX: CoordType = (1 << (CoordType::BITS / 2 - 1)) - 1;
|
||||
|
||||
/// A 2D point. Uses [`CoordType`].
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
@ -58,11 +58,11 @@ pub struct Point {
|
|||
}
|
||||
|
||||
impl Point {
|
||||
pub const MIN: Point = Point { x: CoordType::MIN, y: CoordType::MIN };
|
||||
pub const MAX: Point = Point { x: CoordType::MAX, y: CoordType::MAX };
|
||||
pub const MIN: Self = Self { x: CoordType::MIN, y: CoordType::MIN };
|
||||
pub const MAX: Self = Self { x: CoordType::MAX, y: CoordType::MAX };
|
||||
}
|
||||
|
||||
impl PartialOrd<Point> for Point {
|
||||
impl PartialOrd<Self> for Point {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
|
@ -70,10 +70,7 @@ impl PartialOrd<Point> for Point {
|
|||
|
||||
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,
|
||||
}
|
||||
self.y.cmp(&other.y).then(self.x.cmp(&other.x))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -149,7 +146,7 @@ impl Rect {
|
|||
let r = l.max(r);
|
||||
let b = t.max(b);
|
||||
|
||||
Rect { left: l, top: t, right: r, bottom: b }
|
||||
Self { left: l, top: t, right: r, bottom: b }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
30
src/icu.rs
30
src/icu.rs
|
@ -23,6 +23,9 @@ pub fn get_available_encodings() -> &'static [&'static str] {
|
|||
#[allow(static_mut_refs)]
|
||||
unsafe {
|
||||
if ENCODINGS.is_empty() {
|
||||
ENCODINGS.push("UTF-8");
|
||||
ENCODINGS.push("UTF-8 BOM");
|
||||
|
||||
if let Ok(f) = init_if_needed() {
|
||||
let mut n = 0;
|
||||
loop {
|
||||
|
@ -30,14 +33,17 @@ pub fn get_available_encodings() -> &'static [&'static str] {
|
|||
if name.is_null() {
|
||||
break;
|
||||
}
|
||||
ENCODINGS.push(CStr::from_ptr(name).to_str().unwrap_unchecked());
|
||||
|
||||
let name = CStr::from_ptr(name).to_str().unwrap_unchecked();
|
||||
// We have already pushed UTF-8 above.
|
||||
// There is no need to filter UTF-8 BOM here, since ICU does not distinguish it from UTF-8.
|
||||
if name != "UTF-8" {
|
||||
ENCODINGS.push(name);
|
||||
}
|
||||
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if ENCODINGS.is_empty() {
|
||||
ENCODINGS.push("UTF-8");
|
||||
}
|
||||
}
|
||||
&ENCODINGS
|
||||
}
|
||||
|
@ -622,17 +628,25 @@ impl Regex {
|
|||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that the given `Text` outlives the `Regex` instance.
|
||||
pub unsafe fn set_text(&mut self, text: &Text) {
|
||||
pub unsafe fn set_text(&mut self, text: &mut Text, offset: usize) {
|
||||
// Get `utext_access_impl` to detect the `TextBuffer::generation` change,
|
||||
// and refresh its contents. This ensures that ICU doesn't reuse
|
||||
// stale `UText::chunk_contents`, as it has no way tell that it's stale.
|
||||
utext_access(text.0, offset as i64, true);
|
||||
|
||||
let f = assume_loaded();
|
||||
let mut status = icu_ffi::U_ZERO_ERROR;
|
||||
unsafe { (f.uregex_setUText)(self.0, text.0 as *const _ as *mut _, &mut status) };
|
||||
// `uregex_setUText` resets the regex to the start of the text.
|
||||
// Because of this, we must also call `uregex_reset64`.
|
||||
unsafe { (f.uregex_reset64)(self.0, offset as i64, &mut status) };
|
||||
}
|
||||
|
||||
/// Sets the regex to the absolute offset in the underlying text.
|
||||
pub fn reset(&mut self, index: usize) {
|
||||
pub fn reset(&mut self, offset: usize) {
|
||||
let f = assume_loaded();
|
||||
let mut status = icu_ffi::U_ZERO_ERROR;
|
||||
unsafe { (f.uregex_reset64)(self.0, index as i64, &mut status) };
|
||||
unsafe { (f.uregex_reset64)(self.0, offset as i64, &mut status) };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
33
src/input.rs
33
src/input.rs
|
@ -12,7 +12,7 @@ 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.
|
||||
/// The reason it's an awkward u32 and not a struct is to hopefully make ABIs easier later.
|
||||
/// Of course you could just translate on the ABI boundary, but my hope is that this
|
||||
/// design lets me realize some restrictions early on that I can't foresee yet.
|
||||
#[repr(transparent)]
|
||||
|
@ -40,8 +40,8 @@ impl InputKey {
|
|||
self.0
|
||||
}
|
||||
|
||||
pub(crate) const fn key(&self) -> InputKey {
|
||||
InputKey(self.0 & 0x00FFFFFF)
|
||||
pub(crate) const fn key(&self) -> Self {
|
||||
Self(self.0 & 0x00FFFFFF)
|
||||
}
|
||||
|
||||
pub(crate) const fn modifiers(&self) -> InputKeyMod {
|
||||
|
@ -52,8 +52,8 @@ impl InputKey {
|
|||
(self.0 & modifier.0) != 0
|
||||
}
|
||||
|
||||
pub(crate) const fn with_modifiers(&self, modifiers: InputKeyMod) -> InputKey {
|
||||
InputKey(self.0 | modifiers.0)
|
||||
pub(crate) const fn with_modifiers(&self, modifiers: InputKeyMod) -> Self {
|
||||
Self(self.0 | modifiers.0)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -67,16 +67,16 @@ impl InputKeyMod {
|
|||
Self(v)
|
||||
}
|
||||
|
||||
pub(crate) const fn contains(&self, modifier: InputKeyMod) -> bool {
|
||||
pub(crate) const fn contains(&self, modifier: Self) -> bool {
|
||||
(self.0 & modifier.0) != 0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitOr<InputKeyMod> for InputKey {
|
||||
type Output = InputKey;
|
||||
type Output = Self;
|
||||
|
||||
fn bitor(self, rhs: InputKeyMod) -> InputKey {
|
||||
InputKey(self.0 | rhs.0)
|
||||
fn bitor(self, rhs: InputKeyMod) -> Self {
|
||||
Self(self.0 | rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -302,16 +302,15 @@ impl Parser {
|
|||
}
|
||||
|
||||
/// An iterator that parses VT sequences into input events.
|
||||
///
|
||||
/// Can't implement [`Iterator`], because this is a "lending iterator".
|
||||
pub struct Stream<'parser, 'vt, 'input> {
|
||||
parser: &'parser mut Parser,
|
||||
stream: vt::Stream<'vt, 'input>,
|
||||
}
|
||||
|
||||
impl<'input> Stream<'_, '_, 'input> {
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn next(&mut self) -> Option<Input<'input>> {
|
||||
impl<'input> Iterator for Stream<'_, '_, 'input> {
|
||||
type Item = Input<'input>;
|
||||
|
||||
fn next(&mut self) -> Option<Input<'input>> {
|
||||
loop {
|
||||
if self.parser.bracketed_paste {
|
||||
return self.handle_bracketed_paste();
|
||||
|
@ -489,7 +488,9 @@ impl<'input> Stream<'_, '_, 'input> {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'input> Stream<'_, '_, 'input> {
|
||||
/// Once we encounter the start of a bracketed paste
|
||||
/// we seek to the end of the paste in this function.
|
||||
///
|
||||
|
@ -498,8 +499,8 @@ impl<'input> Stream<'_, '_, 'input> {
|
|||
/// <ESC>[201~ lots of text <ESC>[201~
|
||||
/// ```
|
||||
///
|
||||
/// That text inbetween is then expected to be taken literally.
|
||||
/// It can inbetween be anything though, including other escape sequences.
|
||||
/// That in between text is then expected to be taken literally.
|
||||
/// It can be in between anything though, including other escape sequences.
|
||||
/// This is the reason why this is a separate method.
|
||||
#[cold]
|
||||
fn handle_bracketed_paste(&mut self) -> Option<Input<'input>> {
|
||||
|
|
|
@ -145,7 +145,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_page_boundary() {
|
||||
let page = unsafe {
|
||||
const PAGE_SIZE: usize = 64 * 1024; // 64 KiB to cover many architectures.
|
||||
const PAGE_SIZE: usize = 64 * 1024; // 64 KiB to cover many architectures.
|
||||
|
||||
// 3 pages: uncommitted, committed, uncommitted
|
||||
let ptr = sys::virtual_reserve(PAGE_SIZE * 3).unwrap();
|
||||
|
|
|
@ -141,7 +141,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_page_boundary() {
|
||||
let page = unsafe {
|
||||
const PAGE_SIZE: usize = 64 * 1024; // 64 KiB to cover many architectures.
|
||||
const PAGE_SIZE: usize = 64 * 1024; // 64 KiB to cover many architectures.
|
||||
|
||||
// 3 pages: uncommitted, committed, uncommitted
|
||||
let ptr = sys::virtual_reserve(PAGE_SIZE * 3).unwrap();
|
||||
|
|
|
@ -223,8 +223,8 @@ pub fn read_input() -> Option<Input<'static>> {
|
|||
STATE.inject_resize = false;
|
||||
let t = get_window_size();
|
||||
Some(Input::Resize(Size {
|
||||
width: t.0 as i32,
|
||||
height: t.1 as i32,
|
||||
width: t.0 as isize,
|
||||
height: t.1 as isize,
|
||||
}))
|
||||
} else {
|
||||
transform_single_key(&wait_and_read_single_key_ex()?)
|
||||
|
|
|
@ -18,6 +18,21 @@ use crate::arena::{Arena, ArenaString, scratch_arena};
|
|||
use crate::helpers::*;
|
||||
use crate::{apperr, arena_format};
|
||||
|
||||
#[cfg(target_os = "netbsd")]
|
||||
const fn desired_mprotect(flags: c_int) -> c_int {
|
||||
// NetBSD allows an mmap(2) caller to specify what protection flags they
|
||||
// will use later via mprotect. It does not allow a caller to move from
|
||||
// PROT_NONE to PROT_READ | PROT_WRITE.
|
||||
//
|
||||
// see PROT_MPROTECT in man 2 mmap
|
||||
flags << 3
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "netbsd"))]
|
||||
const fn desired_mprotect(_: c_int) -> c_int {
|
||||
libc::PROT_NONE
|
||||
}
|
||||
|
||||
struct State {
|
||||
stdin: libc::c_int,
|
||||
stdin_flags: libc::c_int,
|
||||
|
@ -379,7 +394,7 @@ pub unsafe fn virtual_reserve(size: usize) -> apperr::Result<NonNull<u8>> {
|
|||
let ptr = libc::mmap(
|
||||
null_mut(),
|
||||
size,
|
||||
libc::PROT_NONE,
|
||||
desired_mprotect(libc::PROT_READ | libc::PROT_WRITE),
|
||||
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
|
||||
-1,
|
||||
0,
|
||||
|
@ -521,7 +536,7 @@ where
|
|||
if suffix.is_empty() {
|
||||
name
|
||||
} else {
|
||||
// SAFETY: In this particualar case we know that the string
|
||||
// SAFETY: In this particular case we know that the string
|
||||
// is valid UTF-8, because it comes from icu.rs.
|
||||
let name = unsafe { name.to_str().unwrap_unchecked() };
|
||||
|
||||
|
|
|
@ -397,6 +397,21 @@ pub fn open_stdin_if_redirected() -> Option<File> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn drives() -> impl Iterator<Item = char> {
|
||||
unsafe {
|
||||
let mut mask = FileSystem::GetLogicalDrives();
|
||||
std::iter::from_fn(move || {
|
||||
let bit = mask.trailing_zeros();
|
||||
if bit >= 26 {
|
||||
None
|
||||
} else {
|
||||
mask &= !(1 << bit);
|
||||
Some((b'A' + bit as u8) as char)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A unique identifier for a file.
|
||||
pub enum FileId {
|
||||
Id(FileSystem::FILE_ID_INFO),
|
||||
|
@ -406,14 +421,14 @@ pub enum FileId {
|
|||
impl PartialEq for FileId {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(FileId::Id(left), FileId::Id(right)) => {
|
||||
(Self::Id(left), Self::Id(right)) => {
|
||||
// Lowers to an efficient word-wise comparison.
|
||||
const SIZE: usize = std::mem::size_of::<FileSystem::FILE_ID_INFO>();
|
||||
let a: &[u8; SIZE] = unsafe { mem::transmute(left) };
|
||||
let b: &[u8; SIZE] = unsafe { mem::transmute(right) };
|
||||
a == b
|
||||
}
|
||||
(FileId::Path(left), FileId::Path(right)) => left == right,
|
||||
(Self::Path(left), Self::Path(right)) => left == right,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
@ -500,9 +515,11 @@ pub unsafe fn virtual_reserve(size: usize) -> apperr::Result<NonNull<u8>> {
|
|||
///
|
||||
/// This function is unsafe because it uses raw pointers.
|
||||
/// Make sure to only pass pointers acquired from [`virtual_reserve`].
|
||||
pub unsafe fn virtual_release(base: NonNull<u8>, size: usize) {
|
||||
pub unsafe fn virtual_release(base: NonNull<u8>, _size: usize) {
|
||||
unsafe {
|
||||
Memory::VirtualFree(base.as_ptr() as *mut _, size, Memory::MEM_RELEASE);
|
||||
// NOTE: `VirtualFree` fails if the pointer isn't
|
||||
// a valid base address or if the size isn't zero.
|
||||
Memory::VirtualFree(base.as_ptr() as *mut _, 0, Memory::MEM_RELEASE);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
511
src/tui.rs
511
src/tui.rs
|
@ -10,7 +10,7 @@
|
|||
//! fairly minimal, and for that purpose an immediate mode design is much simpler to use.
|
||||
//!
|
||||
//! So what's "immediate mode"? The primary alternative is called "retained mode".
|
||||
//! The diference is that when you create a button in this framework in one frame,
|
||||
//! The difference is that when you create a button in this framework in one frame,
|
||||
//! and you stop telling this framework in the next frame, the button will vanish.
|
||||
//! When you use a regular retained mode UI framework, you create the button once,
|
||||
//! set up callbacks for when it is clicked, and then stop worrying about it.
|
||||
|
@ -131,7 +131,7 @@
|
|||
//!
|
||||
//! // Thanks to the lack of callbacks, we can use a primitive
|
||||
//! // if condition here, as well as in any potential C code.
|
||||
//! if ctx.button("button", "Click me!") {
|
||||
//! if ctx.button("button", "Click me!", ButtonStyle::default()) {
|
||||
//! state.counter += 1;
|
||||
//! }
|
||||
//!
|
||||
|
@ -161,6 +161,8 @@ use crate::{apperr, arena_format, input, unicode};
|
|||
|
||||
const ROOT_ID: u64 = 0x14057B7EF767814F; // Knuth's MMIX constant
|
||||
const SHIFT_TAB: InputKey = vk::TAB.with_modifiers(kbmod::SHIFT);
|
||||
const KBMOD_FOR_WORD_NAV: InputKeyMod =
|
||||
if cfg!(target_os = "macos") { kbmod::ALT } else { kbmod::CTRL };
|
||||
|
||||
type Input<'input> = input::Input<'input>;
|
||||
type InputKey = input::InputKey;
|
||||
|
@ -257,6 +259,41 @@ pub enum Overflow {
|
|||
TruncateTail,
|
||||
}
|
||||
|
||||
/// Controls the style with which a button label renders
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ButtonStyle {
|
||||
accelerator: Option<char>,
|
||||
checked: Option<bool>,
|
||||
bracketed: bool,
|
||||
}
|
||||
|
||||
impl ButtonStyle {
|
||||
/// Draw an accelerator label: `[_E_xample button]` or `[Example button(X)]`
|
||||
///
|
||||
/// Must provide an upper-case ASCII character.
|
||||
pub fn accelerator(self, char: char) -> Self {
|
||||
Self { accelerator: Some(char), ..self }
|
||||
}
|
||||
/// Draw a checkbox prefix: `[🗹 Example Button]`
|
||||
pub fn checked(self, checked: bool) -> Self {
|
||||
Self { checked: Some(checked), ..self }
|
||||
}
|
||||
/// Draw with or without brackets: `[Example Button]` or `Example Button`
|
||||
pub fn bracketed(self, bracketed: bool) -> Self {
|
||||
Self { bracketed, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ButtonStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
accelerator: None,
|
||||
checked: None,
|
||||
bracketed: true, // Default style for most buttons. Brackets may be disabled e.g. for buttons in menus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// There's two types of lifetimes the TUI code needs to manage:
|
||||
/// * Across frames
|
||||
/// * Per frame
|
||||
|
@ -486,17 +523,21 @@ impl Tui {
|
|||
self.mouse_is_drag = false;
|
||||
}
|
||||
|
||||
if self.scroll_to_focused() {
|
||||
self.needs_more_settling();
|
||||
}
|
||||
|
||||
//let now = std::time::Instant::now();
|
||||
let mut input_text = None;
|
||||
let mut input_keyboard = None;
|
||||
let mut input_mouse_modifiers = kbmod::NONE;
|
||||
let mut input_mouse_click = 0;
|
||||
let mut input_scroll_delta = Point { x: 0, y: 0 };
|
||||
let input_consumed = self.needs_settling();
|
||||
// `input_consumed` should be `true` if we're in the settling phase which is indicated by
|
||||
// `self.needs_settling() == true`. However, there's a possibility for it being true from
|
||||
// a previous frame, and we do have fresh new input. In that case want `input_consumed`
|
||||
// to be false of course which is ensured by checking for `input.is_none()`.
|
||||
let input_consumed = self.needs_settling() && input.is_none();
|
||||
|
||||
if self.scroll_to_focused() {
|
||||
self.needs_more_settling();
|
||||
}
|
||||
|
||||
match input {
|
||||
None => {}
|
||||
|
@ -640,6 +681,7 @@ impl Tui {
|
|||
|
||||
tree,
|
||||
last_modal: None,
|
||||
focused_node: None,
|
||||
next_block_id_mixin: 0,
|
||||
needs_settling: false,
|
||||
|
||||
|
@ -651,7 +693,13 @@ impl Tui {
|
|||
fn report_context_completion<'a>(&'a mut self, ctx: &mut Context<'a, '_>) {
|
||||
// If this hits, you forgot to block_end() somewhere. The best way to figure
|
||||
// out where is to do a binary search of commenting out code in main.rs.
|
||||
debug_assert!(ctx.tree.current_node.borrow().stack_parent.is_none());
|
||||
debug_assert!(
|
||||
ctx.tree.current_node.borrow().stack_parent.is_none(),
|
||||
"Dangling parent! Did you miss a block_end?"
|
||||
);
|
||||
|
||||
// End the root node.
|
||||
ctx.block_end();
|
||||
|
||||
// Ensure that focus doesn't escape the active modal.
|
||||
if let Some(node) = ctx.last_modal
|
||||
|
@ -684,17 +732,7 @@ impl Tui {
|
|||
// Remove any unknown nodes from the focus path.
|
||||
// It's important that we do this after the tree has been swapped out,
|
||||
// so that pop_focusable_node() has access to the newest version of the tree.
|
||||
let focus_path_changed = self.pop_focusable_node(focus_path_pop_min);
|
||||
needs_settling |= focus_path_changed;
|
||||
|
||||
// If some elements went away and the focus path changed above, we ignore tab presses.
|
||||
// It may otherwise lead to weird situations where focus moves unexpectedly.
|
||||
if !focus_path_changed
|
||||
&& !ctx.input_consumed
|
||||
&& let Some(input) = ctx.input_keyboard
|
||||
{
|
||||
needs_settling |= self.move_focus(input);
|
||||
}
|
||||
needs_settling |= self.pop_focusable_node(focus_path_pop_min);
|
||||
|
||||
// `needs_more_settling()` depends on the current value
|
||||
// of `settling_have` and so we increment it first.
|
||||
|
@ -1222,117 +1260,6 @@ impl Tui {
|
|||
last_before != last_after
|
||||
}
|
||||
|
||||
// TODO: Move this into `block_end()` and run it whenever the block is a `focus_well`.
|
||||
// It makes no sense otherwise that all input handling occurs in the controls, except for this.
|
||||
fn move_focus(&mut self, input: InputKey) -> bool {
|
||||
if !matches!(input, vk::TAB | SHIFT_TAB | vk::UP | vk::DOWN | vk::LEFT | vk::RIGHT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let focused_id = self.focused_node_path.last().cloned().unwrap_or(0);
|
||||
let Some(focused) = self.prev_node_map.get(focused_id) else {
|
||||
debug_assert!(false); // The caller should've cleaned up the focus path.
|
||||
return false;
|
||||
};
|
||||
|
||||
let mut focused_start = focused;
|
||||
let mut root = focused;
|
||||
|
||||
// Figure out if we're inside a focus void (a container that doesn't
|
||||
// allow tabbing inside), and in that case, toss the focus to it.
|
||||
//
|
||||
// Also, figure out the container within which the focus must be contained.
|
||||
// This way, tab/shift-tab only moves within the same window.
|
||||
// The ROOT_ID node has no parent, and the others have a float attribute.
|
||||
// If the root is the focused node, it should of course not move upward.
|
||||
loop {
|
||||
let root_node = root.borrow();
|
||||
if root_node.attributes.focus_well {
|
||||
break;
|
||||
}
|
||||
if root_node.attributes.focus_void {
|
||||
focused_start = root;
|
||||
}
|
||||
root = match root_node.parent {
|
||||
Some(parent) => parent,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
let forward;
|
||||
let min_depth;
|
||||
match input {
|
||||
SHIFT_TAB | vk::TAB => {
|
||||
forward = input == vk::TAB;
|
||||
min_depth = usize::MAX;
|
||||
}
|
||||
vk::UP | vk::DOWN => {
|
||||
forward = input == vk::DOWN;
|
||||
min_depth = usize::MAX;
|
||||
}
|
||||
vk::LEFT | vk::RIGHT => {
|
||||
// Find the cell within a row within a table that we're in.
|
||||
// To do so we'll use a circular buffer of the last 3 nodes while we travel up.
|
||||
let mut buf = [None; 3];
|
||||
let mut idx = buf.len() - 1;
|
||||
let mut node = focused_start;
|
||||
|
||||
loop {
|
||||
idx = (idx + 1) % buf.len();
|
||||
buf[idx] = Some(node);
|
||||
if let NodeContent::Table(..) = &node.borrow().content {
|
||||
break;
|
||||
}
|
||||
if ptr::eq(node, root) {
|
||||
return false;
|
||||
}
|
||||
node = match node.borrow().parent {
|
||||
Some(parent) => parent,
|
||||
None => return false,
|
||||
}
|
||||
}
|
||||
|
||||
// The current `idx` points to the table.
|
||||
// The last item is the row.
|
||||
// The 2nd to last item is the cell.
|
||||
let Some(row) = buf[(idx + 3 - 1) % buf.len()] else {
|
||||
return false;
|
||||
};
|
||||
let Some(cell) = buf[(idx + 3 - 2) % buf.len()] else {
|
||||
return false;
|
||||
};
|
||||
|
||||
root = row;
|
||||
focused_start = cell;
|
||||
forward = input == vk::RIGHT;
|
||||
min_depth = root.borrow().depth;
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
let mut focused_next = focused_start;
|
||||
Tree::visit_all(root, focused_start, forward, |node| {
|
||||
let n = node.borrow();
|
||||
if ptr::eq(node, root) {
|
||||
VisitControl::Continue
|
||||
} else if n.attributes.focusable && !ptr::eq(node, focused_start) {
|
||||
focused_next = node;
|
||||
VisitControl::Stop
|
||||
} else if n.attributes.focus_void || n.depth >= min_depth {
|
||||
VisitControl::SkipChildren
|
||||
} else {
|
||||
VisitControl::Continue
|
||||
}
|
||||
});
|
||||
|
||||
if ptr::eq(focused_next, focused_start) {
|
||||
false
|
||||
} else {
|
||||
Tui::build_node_path(Some(focused_next), &mut self.focused_node_path);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll the focused node(s) into view inside scrollviews
|
||||
fn scroll_to_focused(&mut self) -> bool {
|
||||
let focused_id = self.focused_node_path.last().cloned().unwrap_or(0);
|
||||
|
@ -1384,6 +1311,7 @@ pub struct Context<'a, 'input> {
|
|||
|
||||
tree: Tree<'a>,
|
||||
last_modal: Option<&'a NodeCell<'a>>,
|
||||
focused_node: Option<&'a NodeCell<'a>>,
|
||||
next_block_id_mixin: u64,
|
||||
needs_settling: bool,
|
||||
|
||||
|
@ -1493,6 +1421,85 @@ impl<'a> Context<'a, '_> {
|
|||
/// Ends the current UI block, returning to its parent container.
|
||||
pub fn block_end(&mut self) {
|
||||
self.tree.pop_stack();
|
||||
self.block_end_move_focus();
|
||||
}
|
||||
|
||||
fn block_end_move_focus(&mut self) {
|
||||
// At this point, it's more like "focus_well?" instead of "focus_well!".
|
||||
let focus_well = self.tree.last_node;
|
||||
|
||||
// Remember the focused node, if any, because once the code below runs,
|
||||
// we need it for the `Tree::visit_all` call.
|
||||
if self.is_focused() {
|
||||
self.focused_node = Some(focus_well);
|
||||
}
|
||||
|
||||
// The mere fact that there's a `focused_node` indicates that we're the
|
||||
// first `block_end()` call that's a focus well and also contains the focus.
|
||||
let Some(focused) = self.focused_node else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Filter down to nodes that are focus wells and contain the focus. They're
|
||||
// basically the "tab container". We test for the node depth to ensure that
|
||||
// we don't accidentally pick a focus well next to or inside the focused node.
|
||||
{
|
||||
let n = focus_well.borrow();
|
||||
if !n.attributes.focus_well || n.depth > focused.borrow().depth {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter down to Tab/Shift+Tab inputs.
|
||||
if self.input_consumed {
|
||||
return;
|
||||
}
|
||||
let Some(input) = self.input_keyboard else {
|
||||
return;
|
||||
};
|
||||
if !matches!(input, SHIFT_TAB | vk::TAB) {
|
||||
return;
|
||||
}
|
||||
|
||||
let forward = input == vk::TAB;
|
||||
let mut focused_start = focused;
|
||||
let mut focused_next = focused;
|
||||
|
||||
// We may be in a focus void right now (= doesn't want to be tabbed into),
|
||||
// so first we must go up the tree until we're outside of it.
|
||||
loop {
|
||||
if ptr::eq(focused_start, focus_well) {
|
||||
// If we hit the root / focus well, we weren't in a focus void,
|
||||
// and can reset `focused_before` to the current focused node.
|
||||
focused_start = focused;
|
||||
break;
|
||||
}
|
||||
|
||||
focused_start = focused_start.borrow().parent.unwrap();
|
||||
if focused_start.borrow().attributes.focus_void {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Tree::visit_all(focus_well, focused_start, forward, |node| {
|
||||
let n = node.borrow();
|
||||
if n.attributes.focusable && !ptr::eq(node, focused_start) {
|
||||
focused_next = node;
|
||||
VisitControl::Stop
|
||||
} else if n.attributes.focus_void {
|
||||
VisitControl::SkipChildren
|
||||
} else {
|
||||
VisitControl::Continue
|
||||
}
|
||||
});
|
||||
|
||||
if ptr::eq(focused_next, focused_start) {
|
||||
return;
|
||||
}
|
||||
|
||||
Tui::build_node_path(Some(focused_next), &mut self.tui.focused_node_path);
|
||||
self.set_input_consumed();
|
||||
self.needs_rerender();
|
||||
}
|
||||
|
||||
/// Mixes in an extra value to the next UI block's ID for uniqueness.
|
||||
|
@ -1805,6 +1812,8 @@ impl<'a> Context<'a, '_> {
|
|||
debug_assert!(matches!(parent.content, NodeContent::Table(_)));
|
||||
|
||||
self.block_end();
|
||||
self.table_end_row();
|
||||
|
||||
self.next_block_id_mixin(parent.child_count as u64);
|
||||
}
|
||||
}
|
||||
|
@ -1812,6 +1821,10 @@ impl<'a> Context<'a, '_> {
|
|||
self.block_begin("row");
|
||||
}
|
||||
|
||||
fn table_end_row(&mut self) {
|
||||
self.table_move_focus(vk::LEFT, vk::RIGHT);
|
||||
}
|
||||
|
||||
/// Ends the current table block.
|
||||
pub fn table_end(&mut self) {
|
||||
let current_node = self.tree.current_node.borrow();
|
||||
|
@ -1820,9 +1833,70 @@ impl<'a> Context<'a, '_> {
|
|||
// current_node will refer to the table. Otherwise, it'll refer to the current row.
|
||||
if !matches!(current_node.content, NodeContent::Table(_)) {
|
||||
self.block_end();
|
||||
self.table_end_row();
|
||||
}
|
||||
|
||||
self.block_end(); // table
|
||||
self.table_move_focus(vk::UP, vk::DOWN);
|
||||
}
|
||||
|
||||
fn table_move_focus(&mut self, prev_key: InputKey, next_key: InputKey) {
|
||||
// Filter down to table rows that are focused.
|
||||
if !self.contains_focus() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter down to our prev/next inputs.
|
||||
if self.input_consumed {
|
||||
return;
|
||||
}
|
||||
let Some(input) = self.input_keyboard else {
|
||||
return;
|
||||
};
|
||||
if input != prev_key && input != next_key {
|
||||
return;
|
||||
}
|
||||
|
||||
let container = self.tree.last_node;
|
||||
let Some(&focused_id) = self.tui.focused_node_path.get(container.borrow().depth + 1) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut prev_next = NodeSiblings { prev: None, next: None };
|
||||
let mut focused = None;
|
||||
|
||||
// Iterate through the cells in the row / the rows in the table, looking for focused_id.
|
||||
// Take note of the previous and next focusable cells / rows around the focused one.
|
||||
for cell in Tree::iterate_siblings(container.borrow().children.first) {
|
||||
let n = cell.borrow();
|
||||
if n.id == focused_id {
|
||||
focused = Some(cell);
|
||||
} else if n.attributes.focusable {
|
||||
if focused.is_none() {
|
||||
prev_next.prev = Some(cell);
|
||||
} else {
|
||||
prev_next.next = Some(cell);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if focused.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let forward = input == next_key;
|
||||
let children_idx = if forward { NodeChildren::FIRST } else { NodeChildren::LAST };
|
||||
let siblings_idx = if forward { NodeSiblings::NEXT } else { NodeSiblings::PREV };
|
||||
let Some(focused_next) =
|
||||
prev_next.get(siblings_idx).or_else(|| container.borrow().children.get(children_idx))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
Tui::build_node_path(Some(focused_next), &mut self.tui.focused_node_path);
|
||||
self.set_input_consumed();
|
||||
self.needs_rerender();
|
||||
}
|
||||
|
||||
/// Creates a simple text label.
|
||||
|
@ -1927,17 +2001,12 @@ impl<'a> Context<'a, '_> {
|
|||
|
||||
/// Creates a button with the given text.
|
||||
/// Returns true if the button was activated.
|
||||
pub fn button(&mut self, classname: &'static str, text: &str) -> bool {
|
||||
self.styled_label_begin(classname);
|
||||
pub fn button(&mut self, classname: &'static str, text: &str, style: ButtonStyle) -> bool {
|
||||
self.button_label(classname, text, style);
|
||||
self.attr_focusable();
|
||||
if self.is_focused() {
|
||||
self.attr_reverse();
|
||||
}
|
||||
self.styled_label_add_text("[");
|
||||
self.styled_label_add_text(text);
|
||||
self.styled_label_add_text("]");
|
||||
self.styled_label_end();
|
||||
|
||||
self.button_activated()
|
||||
}
|
||||
|
||||
|
@ -1949,7 +2018,7 @@ impl<'a> Context<'a, '_> {
|
|||
if self.is_focused() {
|
||||
self.attr_reverse();
|
||||
}
|
||||
self.styled_label_add_text(if *checked { "[▣ " } else { "[☐ " });
|
||||
self.styled_label_add_text(if *checked { "[🗹 " } else { "[☐ " });
|
||||
self.styled_label_add_text(text);
|
||||
self.styled_label_add_text("]");
|
||||
self.styled_label_end();
|
||||
|
@ -2124,6 +2193,7 @@ impl<'a> Context<'a, '_> {
|
|||
let mut tb = tc.buffer.borrow_mut();
|
||||
let tb = &mut *tb;
|
||||
let mut make_cursor_visible = false;
|
||||
let mut change_preferred_column = false;
|
||||
|
||||
if self.tui.mouse_state != InputMouseState::None
|
||||
&& self.tui.was_mouse_down_on_node(node_prev.id)
|
||||
|
@ -2230,7 +2300,8 @@ impl<'a> Context<'a, '_> {
|
|||
let trackable = track_rect.height() - tc.thumb_height;
|
||||
let delta_y = mouse.y - self.tui.mouse_down_position.y;
|
||||
tc.scroll_offset.y = tc.scroll_offset_y_drag_start
|
||||
+ ((delta_y * scrollable_height) / trackable);
|
||||
+ (delta_y as i64 * scrollable_height as i64 / trackable as i64)
|
||||
as CoordType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2435,7 +2506,7 @@ impl<'a> Context<'a, '_> {
|
|||
}
|
||||
}
|
||||
vk::LEFT => {
|
||||
let granularity = if modifiers.contains(kbmod::CTRL) {
|
||||
let granularity = if modifiers.contains(KBMOD_FOR_WORD_NAV) {
|
||||
CursorMovement::Word
|
||||
} else {
|
||||
CursorMovement::Grapheme
|
||||
|
@ -2493,7 +2564,7 @@ impl<'a> Context<'a, '_> {
|
|||
}
|
||||
}
|
||||
vk::RIGHT => {
|
||||
let granularity = if modifiers.contains(kbmod::CTRL) {
|
||||
let granularity = if modifiers.contains(KBMOD_FOR_WORD_NAV) {
|
||||
CursorMovement::Word
|
||||
} else {
|
||||
CursorMovement::Grapheme
|
||||
|
@ -2574,6 +2645,22 @@ impl<'a> Context<'a, '_> {
|
|||
kbmod::CTRL => tb.select_all(),
|
||||
_ => return false,
|
||||
},
|
||||
vk::B => match modifiers {
|
||||
kbmod::ALT if cfg!(target_os = "macos") => {
|
||||
// On macOS, terminals commonly emit the Emacs style
|
||||
// Alt+B (ESC b) sequence for Alt+Left.
|
||||
tb.cursor_move_delta(CursorMovement::Word, -1);
|
||||
}
|
||||
_ => return false,
|
||||
},
|
||||
vk::F => match modifiers {
|
||||
kbmod::ALT if cfg!(target_os = "macos") => {
|
||||
// On macOS, terminals commonly emit the Emacs style
|
||||
// Alt+F (ESC f) sequence for Alt+Right.
|
||||
tb.cursor_move_delta(CursorMovement::Word, 1);
|
||||
}
|
||||
_ => return false,
|
||||
},
|
||||
vk::H => match modifiers {
|
||||
kbmod::CTRL => tb.delete(CursorMovement::Word, -1),
|
||||
_ => return false,
|
||||
|
@ -2606,9 +2693,7 @@ impl<'a> Context<'a, '_> {
|
|||
_ => return false,
|
||||
}
|
||||
|
||||
if !matches!(key, vk::PRIOR | vk::NEXT | vk::UP | vk::DOWN) {
|
||||
tc.preferred_column = tb.cursor_visual_pos().x;
|
||||
}
|
||||
change_preferred_column = !matches!(key, vk::PRIOR | vk::NEXT | vk::UP | vk::DOWN);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
@ -2619,6 +2704,11 @@ impl<'a> Context<'a, '_> {
|
|||
}
|
||||
if !write.is_empty() {
|
||||
tb.write(write, write_raw);
|
||||
change_preferred_column = true;
|
||||
}
|
||||
|
||||
if change_preferred_column {
|
||||
tc.preferred_column = tb.cursor_visual_pos().x;
|
||||
}
|
||||
|
||||
self.set_input_consumed();
|
||||
|
@ -2754,7 +2844,9 @@ impl<'a> Context<'a, '_> {
|
|||
let delta_y =
|
||||
self.tui.mouse_position.y - self.tui.mouse_down_position.y;
|
||||
sc.scroll_offset.y = sc.scroll_offset_y_drag_start
|
||||
+ ((delta_y * scrollable_height) / trackable);
|
||||
+ (delta_y as i64 * scrollable_height as i64
|
||||
/ trackable as i64)
|
||||
as CoordType;
|
||||
}
|
||||
|
||||
self.set_input_consumed();
|
||||
|
@ -3004,15 +3096,21 @@ impl<'a> Context<'a, '_> {
|
|||
///
|
||||
/// Returns true if the menu is open. Continue appending items to it in that case.
|
||||
pub fn menubar_menu_begin(&mut self, text: &str, accelerator: char) -> bool {
|
||||
let accelerator = if cfg!(target_os = "macos") { '\0' } else { accelerator };
|
||||
let mixin = self.tree.current_node.borrow().child_count as u64;
|
||||
self.next_block_id_mixin(mixin);
|
||||
|
||||
self.menubar_label(text, accelerator, None);
|
||||
self.button_label(
|
||||
"menu_button",
|
||||
text,
|
||||
ButtonStyle::default().accelerator(accelerator).bracketed(false),
|
||||
);
|
||||
self.attr_focusable();
|
||||
self.attr_padding(Rect::two(0, 1));
|
||||
|
||||
let contains_focus = self.contains_focus();
|
||||
let keyboard_focus = !contains_focus
|
||||
let keyboard_focus = accelerator != '\0'
|
||||
&& !contains_focus
|
||||
&& self.consume_shortcut(kbmod::ALT | InputKey::new(accelerator as u32));
|
||||
|
||||
if contains_focus || keyboard_focus {
|
||||
|
@ -3081,7 +3179,11 @@ impl<'a> Context<'a, '_> {
|
|||
let clicked =
|
||||
self.button_activated() || self.consume_shortcut(InputKey::new(accelerator as u32));
|
||||
|
||||
self.menubar_label(text, accelerator, Some(checked));
|
||||
self.button_label(
|
||||
"menu_checkbox",
|
||||
text,
|
||||
ButtonStyle::default().bracketed(false).checked(checked).accelerator(accelerator),
|
||||
);
|
||||
self.menubar_shortcut(shortcut);
|
||||
|
||||
if clicked {
|
||||
|
@ -3099,9 +3201,11 @@ impl<'a> Context<'a, '_> {
|
|||
|
||||
if !self.input_consumed
|
||||
&& let Some(key) = self.input_keyboard
|
||||
&& matches!(key, vk::ESCAPE | vk::UP | vk::DOWN | vk::LEFT | vk::RIGHT)
|
||||
&& matches!(key, vk::ESCAPE | vk::UP | vk::DOWN)
|
||||
{
|
||||
if matches!(key, vk::UP | vk::DOWN) {
|
||||
// If the focus is on the menubar, and the user presses up/down,
|
||||
// focus the first/last item of the flyout respectively.
|
||||
let ln = self.tree.last_node.borrow();
|
||||
if self.tui.is_node_focused(ln.parent.map_or(0, |n| n.borrow().id)) {
|
||||
let selected_next =
|
||||
|
@ -3112,14 +3216,9 @@ impl<'a> Context<'a, '_> {
|
|||
}
|
||||
}
|
||||
} else if self.contains_focus() {
|
||||
if key == vk::ESCAPE {
|
||||
// TODO: This should reassign the previous focused path.
|
||||
self.needs_rerender();
|
||||
self.set_input_consumed();
|
||||
Tui::clean_node_path(&mut self.tui.focused_node_path);
|
||||
} else if !self.is_focused() {
|
||||
self.tui.pop_focusable_node(2);
|
||||
}
|
||||
// Otherwise, if the menu is the focused one and the
|
||||
// user presses Escape, pass focus back to the menubar.
|
||||
self.tui.pop_focusable_node(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3129,51 +3228,64 @@ impl<'a> Context<'a, '_> {
|
|||
self.table_end();
|
||||
}
|
||||
|
||||
fn menubar_label(&mut self, text: &str, accelerator: char, checked: Option<bool>) {
|
||||
if !accelerator.is_ascii_uppercase() {
|
||||
self.label("label", text);
|
||||
return;
|
||||
/// Renders a button label with an optional accelerator character
|
||||
/// May also renders a checkbox or square brackets for inline buttons
|
||||
fn button_label(&mut self, classname: &'static str, text: &str, style: ButtonStyle) {
|
||||
// Label prefix
|
||||
self.styled_label_begin(classname);
|
||||
if style.bracketed {
|
||||
self.styled_label_add_text("[");
|
||||
}
|
||||
if let Some(checked) = style.checked {
|
||||
self.styled_label_add_text(if checked { "🗹 " } else { " " });
|
||||
}
|
||||
// Label text
|
||||
match style.accelerator {
|
||||
Some(accelerator) if accelerator.is_ascii_uppercase() => {
|
||||
// Complex case:
|
||||
// Locate the offset of the accelerator character in the label text
|
||||
let mut off = text.len();
|
||||
for (i, c) in text.bytes().enumerate() {
|
||||
// Perfect match (uppercase character) --> stop
|
||||
if c as char == accelerator {
|
||||
off = i;
|
||||
break;
|
||||
}
|
||||
// Inexact match (lowercase character) --> use first hit
|
||||
if (c & !0x20) as char == accelerator && off == text.len() {
|
||||
off = i;
|
||||
}
|
||||
}
|
||||
|
||||
let mut off = text.len();
|
||||
|
||||
for (i, c) in text.bytes().enumerate() {
|
||||
// Perfect match (uppercase character) --> stop
|
||||
if c as char == accelerator {
|
||||
off = i;
|
||||
break;
|
||||
if off < text.len() {
|
||||
// Add an underline to the accelerator.
|
||||
self.styled_label_add_text(&text[..off]);
|
||||
self.styled_label_set_attributes(Attributes::Underlined);
|
||||
self.styled_label_add_text(&text[off..off + 1]);
|
||||
self.styled_label_set_attributes(Attributes::None);
|
||||
self.styled_label_add_text(&text[off + 1..]);
|
||||
} else {
|
||||
// Add the accelerator in parentheses and underline it.
|
||||
let ch = accelerator as u8;
|
||||
self.styled_label_add_text(text);
|
||||
self.styled_label_add_text("(");
|
||||
self.styled_label_set_attributes(Attributes::Underlined);
|
||||
self.styled_label_add_text(unsafe { str_from_raw_parts(&ch, 1) });
|
||||
self.styled_label_set_attributes(Attributes::None);
|
||||
self.styled_label_add_text(")");
|
||||
}
|
||||
}
|
||||
// Inexact match (lowercase character) --> use first hit
|
||||
if (c & !0x20) as char == accelerator && off == text.len() {
|
||||
off = i;
|
||||
_ => {
|
||||
// Simple case:
|
||||
// no accelerator character
|
||||
self.styled_label_add_text(text);
|
||||
}
|
||||
}
|
||||
|
||||
self.styled_label_begin("label");
|
||||
if let Some(checked) = checked {
|
||||
self.styled_label_add_text(if checked { "▣ " } else { " " });
|
||||
// Label postfix
|
||||
if style.bracketed {
|
||||
self.styled_label_add_text("]");
|
||||
}
|
||||
|
||||
if off < text.len() {
|
||||
// Add an underline to the accelerator.
|
||||
self.styled_label_add_text(&text[..off]);
|
||||
self.styled_label_set_attributes(Attributes::Underlined);
|
||||
self.styled_label_add_text(&text[off..off + 1]);
|
||||
self.styled_label_set_attributes(Attributes::None);
|
||||
self.styled_label_add_text(&text[off + 1..]);
|
||||
} else {
|
||||
// Add the accelerator in parentheses and underline it.
|
||||
let ch = accelerator as u8;
|
||||
self.styled_label_add_text(text);
|
||||
self.styled_label_add_text("(");
|
||||
self.styled_label_set_attributes(Attributes::Underlined);
|
||||
self.styled_label_add_text(unsafe { str_from_raw_parts(&ch, 1) });
|
||||
self.styled_label_set_attributes(Attributes::None);
|
||||
self.styled_label_add_text(")");
|
||||
}
|
||||
|
||||
self.styled_label_end();
|
||||
self.attr_padding(Rect { left: 0, top: 0, right: 2, bottom: 0 });
|
||||
}
|
||||
|
||||
fn menubar_shortcut(&mut self, shortcut: InputKey) {
|
||||
|
@ -3199,7 +3311,7 @@ impl<'a> Context<'a, '_> {
|
|||
self.block_begin("shortcut");
|
||||
self.block_end();
|
||||
}
|
||||
self.attr_padding(Rect { left: 0, top: 0, right: 2, bottom: 0 });
|
||||
self.attr_padding(Rect { left: 2, top: 0, right: 2, bottom: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3326,9 +3438,10 @@ impl<'a> Tree<'a> {
|
|||
/// Completes the current node and moves focus to the parent.
|
||||
fn pop_stack(&mut self) {
|
||||
let current_node = self.current_node.borrow();
|
||||
let stack_parent = current_node.stack_parent.unwrap();
|
||||
self.last_node = self.current_node;
|
||||
self.current_node = stack_parent;
|
||||
if let Some(stack_parent) = current_node.stack_parent {
|
||||
self.last_node = self.current_node;
|
||||
self.current_node = stack_parent;
|
||||
}
|
||||
}
|
||||
|
||||
fn iterate_siblings(
|
||||
|
|
|
@ -32,7 +32,7 @@ pub struct Cursor {
|
|||
pub column: CoordType,
|
||||
/// When `measure_forward` hits the `word_wrap_column`, the question is:
|
||||
/// Was there a wrap opportunity on this line? Because if there wasn't,
|
||||
/// a hard-wrap is required, otherwise the word that is being layouted is
|
||||
/// a hard-wrap is required; otherwise, the word that is being laid-out is
|
||||
/// moved to the next line. This boolean carries this state between calls.
|
||||
pub wrap_opp: bool,
|
||||
}
|
||||
|
@ -357,7 +357,7 @@ impl<'doc> MeasurementConfig<'doc> {
|
|||
// Is the word we're currently on so wide that it will be wrapped further down the document?
|
||||
if word_wrap_column > 0 {
|
||||
if !wrap_opp {
|
||||
// If the current layouted line had no wrap opportunities, it means we had an input
|
||||
// If the current laid-out line had no wrap opportunities, it means we had an input
|
||||
// such as "fooooooooooooooooooooo" at a `word_wrap_column` of e.g. 10. The word
|
||||
// didn't fit and the lack of a `wrap_opp` indicates we must force a hard wrap.
|
||||
// Thankfully, if we reach this point, that was already done by the code above.
|
||||
|
@ -944,7 +944,7 @@ mod test {
|
|||
}
|
||||
);
|
||||
|
||||
// Test if the remaining 4 "a"s are properly layouted.
|
||||
// Test if the remaining 4 "a"s are properly laid-out.
|
||||
let end2 = cfg.goto_visual(Point { x: max, y: 2 });
|
||||
assert_eq!(
|
||||
end2,
|
||||
|
@ -968,7 +968,7 @@ mod test {
|
|||
|
||||
for (y, &expected) in expected.iter().enumerate() {
|
||||
let y = y as CoordType;
|
||||
// In order for `goto_visual()` to hit columnn 0 after a word wrap,
|
||||
// In order for `goto_visual()` to hit column 0 after a word wrap,
|
||||
// it MUST be able to go back by 1 grapheme, which is what this tests.
|
||||
let beg = cfg.goto_visual(Point { x: 0, y });
|
||||
let end = cfg.goto_visual(Point { x: 5, y });
|
||||
|
@ -1069,7 +1069,7 @@ mod test {
|
|||
|
||||
for (y, &expected) in expected.iter().enumerate() {
|
||||
let y = y as CoordType;
|
||||
// In order for `goto_visual()` to hit columnn 0 after a word wrap,
|
||||
// In order for `goto_visual()` to hit column 0 after a word wrap,
|
||||
// it MUST be able to go back by 1 grapheme, which is what this tests.
|
||||
let beg = cfg.goto_visual(Point { x: 0, y });
|
||||
let end = cfg.goto_visual(Point { x: 5, y });
|
||||
|
|
125
src/vt.rs
125
src/vt.rs
|
@ -3,9 +3,10 @@
|
|||
|
||||
//! Our VT parser.
|
||||
|
||||
use std::{mem, time};
|
||||
use std::time;
|
||||
|
||||
use crate::simd::memchr2;
|
||||
use crate::unicode::Utf8Chars;
|
||||
|
||||
/// The parser produces these tokens.
|
||||
pub enum Token<'parser, 'input> {
|
||||
|
@ -80,7 +81,7 @@ impl Parser {
|
|||
|
||||
/// Suggests a timeout for the next call to `read()`.
|
||||
///
|
||||
/// We need this because of the ambiguouity of whether a trailing
|
||||
/// We need this because of the ambiguity of whether a trailing
|
||||
/// escape character in an input is starting another escape sequence or
|
||||
/// is just the result of the user literally pressing the Escape key.
|
||||
pub fn read_timeout(&mut self) -> std::time::Duration {
|
||||
|
@ -114,7 +115,7 @@ pub struct Stream<'parser, 'input> {
|
|||
off: usize,
|
||||
}
|
||||
|
||||
impl<'parser, 'input> Stream<'parser, 'input> {
|
||||
impl<'input> Stream<'_, 'input> {
|
||||
/// Returns the input that is being parsed.
|
||||
pub fn input(&self) -> &'input str {
|
||||
self.input
|
||||
|
@ -135,12 +136,19 @@ impl<'parser, 'input> Stream<'parser, 'input> {
|
|||
len
|
||||
}
|
||||
|
||||
fn decode_next(&mut self) -> char {
|
||||
let mut iter = Utf8Chars::new(self.input.as_bytes(), self.off);
|
||||
let c = iter.next().unwrap_or('\0');
|
||||
self.off = iter.offset();
|
||||
c
|
||||
}
|
||||
|
||||
/// Parses the next VT sequence from the previously given input.
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn next(&mut self) -> Option<Token<'parser, 'input>> {
|
||||
// I don't know how to tell Rust that `self.parser` and its lifetime
|
||||
// `'parser` outlives `self`, and at this point I don't care.
|
||||
let parser = unsafe { mem::transmute::<_, &'parser mut Parser>(&mut *self.parser) };
|
||||
#[allow(
|
||||
clippy::should_implement_trait,
|
||||
reason = "can't implement Iterator because this is a lending iterator"
|
||||
)]
|
||||
pub fn next(&mut self) -> Option<Token<'_, 'input>> {
|
||||
let input = self.input;
|
||||
let bytes = input.as_bytes();
|
||||
|
||||
|
@ -148,16 +156,21 @@ impl<'parser, 'input> Stream<'parser, 'input> {
|
|||
// returned `Some(..)` timeout, and if the caller did everything correctly
|
||||
// and there was indeed a timeout, we should be called with an empty
|
||||
// input. In that case we'll return the escape as its own token.
|
||||
if input.is_empty() && matches!(parser.state, State::Esc) {
|
||||
parser.state = State::Ground;
|
||||
if input.is_empty() && matches!(self.parser.state, State::Esc) {
|
||||
self.parser.state = State::Ground;
|
||||
return Some(Token::Esc('\0'));
|
||||
}
|
||||
|
||||
while self.off < bytes.len() {
|
||||
match parser.state {
|
||||
// TODO: The state machine can be roughly broken up into two parts:
|
||||
// * Wants to parse 1 `char` at a time: Ground, Esc, Ss3
|
||||
// These could all be unified to a single call to `decode_next()`.
|
||||
// * Wants to bulk-process bytes: Csi, Osc, Dcs
|
||||
// We should do that so the UTF8 handling is a bit more "unified".
|
||||
match self.parser.state {
|
||||
State::Ground => match bytes[self.off] {
|
||||
0x1b => {
|
||||
parser.state = State::Esc;
|
||||
self.parser.state = State::Esc;
|
||||
self.off += 1;
|
||||
}
|
||||
c @ (0x00..0x20 | 0x7f) => {
|
||||
|
@ -175,45 +188,39 @@ impl<'parser, 'input> Stream<'parser, 'input> {
|
|||
return Some(Token::Text(&input[beg..self.off]));
|
||||
}
|
||||
},
|
||||
State::Esc => {
|
||||
let c = bytes[self.off];
|
||||
self.off += 1;
|
||||
match c {
|
||||
b'[' => {
|
||||
parser.state = State::Csi;
|
||||
parser.csi.private_byte = '\0';
|
||||
parser.csi.final_byte = '\0';
|
||||
while parser.csi.param_count > 0 {
|
||||
parser.csi.param_count -= 1;
|
||||
parser.csi.params[parser.csi.param_count] = 0;
|
||||
}
|
||||
}
|
||||
b']' => {
|
||||
parser.state = State::Osc;
|
||||
}
|
||||
b'O' => {
|
||||
parser.state = State::Ss3;
|
||||
}
|
||||
b'P' => {
|
||||
parser.state = State::Dcs;
|
||||
}
|
||||
c => {
|
||||
parser.state = State::Ground;
|
||||
return Some(Token::Esc(c as char));
|
||||
State::Esc => match self.decode_next() {
|
||||
'[' => {
|
||||
self.parser.state = State::Csi;
|
||||
self.parser.csi.private_byte = '\0';
|
||||
self.parser.csi.final_byte = '\0';
|
||||
while self.parser.csi.param_count > 0 {
|
||||
self.parser.csi.param_count -= 1;
|
||||
self.parser.csi.params[self.parser.csi.param_count] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
']' => {
|
||||
self.parser.state = State::Osc;
|
||||
}
|
||||
'O' => {
|
||||
self.parser.state = State::Ss3;
|
||||
}
|
||||
'P' => {
|
||||
self.parser.state = State::Dcs;
|
||||
}
|
||||
c => {
|
||||
self.parser.state = State::Ground;
|
||||
return Some(Token::Esc(c));
|
||||
}
|
||||
},
|
||||
State::Ss3 => {
|
||||
parser.state = State::Ground;
|
||||
let c = bytes[self.off];
|
||||
self.off += 1;
|
||||
return Some(Token::SS3(c as char));
|
||||
self.parser.state = State::Ground;
|
||||
return Some(Token::SS3(self.decode_next()));
|
||||
}
|
||||
State::Csi => {
|
||||
loop {
|
||||
// If we still have slots left, parse the parameter.
|
||||
if parser.csi.param_count < parser.csi.params.len() {
|
||||
let dst = &mut parser.csi.params[parser.csi.param_count];
|
||||
if self.parser.csi.param_count < self.parser.csi.params.len() {
|
||||
let dst = &mut self.parser.csi.params[self.parser.csi.param_count];
|
||||
while self.off < bytes.len() && bytes[self.off].is_ascii_digit() {
|
||||
let add = bytes[self.off] as u32 - b'0' as u32;
|
||||
let value = *dst as u32 * 10 + add;
|
||||
|
@ -237,15 +244,17 @@ impl<'parser, 'input> Stream<'parser, 'input> {
|
|||
|
||||
match c {
|
||||
0x40..=0x7e => {
|
||||
parser.state = State::Ground;
|
||||
parser.csi.final_byte = c as char;
|
||||
if parser.csi.param_count != 0 || parser.csi.params[0] != 0 {
|
||||
parser.csi.param_count += 1;
|
||||
self.parser.state = State::Ground;
|
||||
self.parser.csi.final_byte = c as char;
|
||||
if self.parser.csi.param_count != 0
|
||||
|| self.parser.csi.params[0] != 0
|
||||
{
|
||||
self.parser.csi.param_count += 1;
|
||||
}
|
||||
return Some(Token::Csi(&parser.csi as &'parser Csi));
|
||||
return Some(Token::Csi(&self.parser.csi));
|
||||
}
|
||||
b';' => parser.csi.param_count += 1,
|
||||
b'<'..=b'?' => parser.csi.private_byte = c as char,
|
||||
b';' => self.parser.csi.param_count += 1,
|
||||
b'<'..=b'?' => self.parser.csi.private_byte = c as char,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -274,7 +283,7 @@ impl<'parser, 'input> Stream<'parser, 'input> {
|
|||
// It's only a string terminator if it's followed by \.
|
||||
// We're at the end so we're saving the state and will continue next time.
|
||||
if self.off >= bytes.len() {
|
||||
parser.state = match parser.state {
|
||||
self.parser.state = match self.parser.state {
|
||||
State::Osc => State::OscEsc,
|
||||
_ => State::DcsEsc,
|
||||
};
|
||||
|
@ -293,9 +302,9 @@ impl<'parser, 'input> Stream<'parser, 'input> {
|
|||
break;
|
||||
}
|
||||
|
||||
let state = parser.state;
|
||||
let state = self.parser.state;
|
||||
if !partial {
|
||||
parser.state = State::Ground;
|
||||
self.parser.state = State::Ground;
|
||||
}
|
||||
return match state {
|
||||
State::Osc => Some(Token::Osc { data, partial }),
|
||||
|
@ -307,10 +316,10 @@ impl<'parser, 'input> Stream<'parser, 'input> {
|
|||
// It's only a string terminator if it's followed by \ (= "\x1b\\").
|
||||
if bytes[self.off] == b'\\' {
|
||||
// It was indeed a string terminator and we can now tell the caller about it.
|
||||
let state = parser.state;
|
||||
let state = self.parser.state;
|
||||
|
||||
// Consume the terminator (one byte in the previous input and this byte).
|
||||
parser.state = State::Ground;
|
||||
self.parser.state = State::Ground;
|
||||
self.off += 1;
|
||||
|
||||
return match state {
|
||||
|
@ -321,11 +330,11 @@ impl<'parser, 'input> Stream<'parser, 'input> {
|
|||
// False alarm: Not a string terminator.
|
||||
// We'll return the escape character as a separate token.
|
||||
// Processing will continue from the current state (`bytes[self.off]`).
|
||||
parser.state = match parser.state {
|
||||
self.parser.state = match self.parser.state {
|
||||
State::OscEsc => State::Osc,
|
||||
_ => State::Dcs,
|
||||
};
|
||||
return match parser.state {
|
||||
return match self.parser.state {
|
||||
State::Osc => Some(Token::Osc { data: "\x1b", partial: true }),
|
||||
_ => Some(Token::Dcs { data: "\x1b", partial: true }),
|
||||
};
|
||||
|
|
|
@ -243,7 +243,7 @@ const HELP: &str = "\
|
|||
Usage: grapheme-table-gen [options...] <ucd.nounihan.grouped.xml>
|
||||
-h, --help Prints help information
|
||||
--lang=<c|rust> Output language (default: c)
|
||||
--extended Expose a start-of-text property for kickstarting the segmentation
|
||||
--extended Expose a start-of-text property for kick-starting the segmentation
|
||||
Expose tab and linefeed as grapheme cluster properties
|
||||
--no-ambiguous Treat all ambiguous characters as narrow
|
||||
--line-breaks Store and expose line break information
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue