Merge remote-tracking branch 'ms/main' into uefi

This commit is contained in:
Dustin L. Howett 2025-06-03 20:12:13 -05:00
commit 1969391b52
43 changed files with 1354 additions and 584 deletions

View file

@ -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.)

View file

@ -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
View 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
View 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

View file

@ -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
View file

@ -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",
]

View file

@ -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 }

View 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.

Binary file not shown.

26
assets/manpage/edit.1 Normal file
View 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

View file

@ -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);

View file

@ -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.")

View file

@ -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)
}
}

View file

@ -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,
}
}

View file

@ -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()
}
}

View file

@ -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)
}

View file

@ -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>) {

View file

@ -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] })
}

View file

@ -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();

View file

@ -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();

View file

@ -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);

View file

@ -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 lenvoyer?",
/* fr */ "Voulez-vous quand même lenvoyer ?",
/* 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 lencodage…",
/* 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;
}
}
}

View file

@ -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.

View file

@ -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);

View file

@ -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 {

View file

@ -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;

View file

@ -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();

View file

@ -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)
}
}

View file

@ -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)) };
}
}

View file

@ -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)
}
}

View file

@ -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

View file

@ -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 }
}
}

View file

@ -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) };
}
}

View file

@ -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>> {

View file

@ -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();

View file

@ -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();

View file

@ -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()?)

View file

@ -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() };

View file

@ -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);
}
}

View file

@ -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(

View file

@ -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
View file

@ -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 }),
};

View file

@ -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