diff options
28 files changed, 2595 insertions, 935 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index cdfaffa6..73c0217c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Default Command+N keybinding for SpawnNewInstance on macOS -- Vi mode for copying text and opening links +- Vi mode for regex search, copying text, and opening links - `CopySelection` action which copies into selection buffer on Linux/BSD - Option `cursor.thickness` to set terminal cursor thickness - Font fallback on Windows @@ -42,6 +42,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Dragging files into terminal now adds a space after each path - Default binding replacement conditions - Adjusted selection clearing granularity to more accurately match content +- To use the cell's text color for selection with a modified background, the `color.selection.text` + variable must now be set to `CellForeground` instead of omitting it ### Fixed @@ -58,6 +60,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Linewrap tracking when switching between primary and alternate screen buffer - Preservation of the alternate screen's saved cursor when swapping to primary screen and back - Reflow of cursor during resize +- Cursor color escape ignored when its color is set to inverted in the config ## 0.4.3 @@ -2,7 +2,7 @@ # It is not intended for manual editing. [[package]] name = "adler32" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -26,15 +26,16 @@ dependencies = [ "font 0.1.0", "gl_generator 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)", "glutin 0.24.1 (registry+https://github.com/rust-lang/crates.io-index)", - "image 0.23.4 (registry+https://github.com/rust-lang/crates.io-index)", + "image 0.23.5 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "notify 4.0.15 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", "rustc_tools_util 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.111 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_yaml 0.8.12 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.112 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.55 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_yaml 0.8.13 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "urlocator 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "wayland-client 0.26.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -46,7 +47,7 @@ dependencies = [ name = "alacritty_terminal" version = "0.5.0-dev" dependencies = [ - "base64 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)", "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "font 0.1.0", "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", @@ -55,14 +56,15 @@ dependencies = [ "mio-anonymous-pipes 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "mio-extras 2.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "mio-named-pipes 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "miow 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "miow 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "nix 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", "objc 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.111 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_yaml 0.8.12 (registry+https://github.com/rust-lang/crates.io-index)", - "signal-hook 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-automata 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.112 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.55 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_yaml 0.8.13 (registry+https://github.com/rust-lang/crates.io-index)", + "signal-hook 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", "terminfo 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "vte 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -106,7 +108,7 @@ name = "approx" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -129,7 +131,7 @@ name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "hermit-abi 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", + "hermit-abi 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -146,7 +148,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "base64" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -165,7 +167,7 @@ dependencies = [ "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "peeking_take_while 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-hash 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "shlex 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -244,7 +246,7 @@ name = "cexpr" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "nom 5.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "nom 5.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -411,7 +413,7 @@ name = "deflate" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "adler32 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -421,8 +423,8 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -464,7 +466,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "dtoa" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -474,8 +476,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.111 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.111 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.112 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.112 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "wio 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -506,7 +508,7 @@ name = "euclid" version = "0.20.13" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -537,7 +539,7 @@ dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", - "miniz_oxide 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "miniz_oxide 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -587,8 +589,8 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -755,7 +757,7 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", @@ -780,23 +782,15 @@ dependencies = [ [[package]] name = "image" -version = "0.23.4" +version = "0.23.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bytemuck 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "num-iter 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", + "num-iter 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)", "num-rational 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "png 0.16.4 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "inflate" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", + "png 0.16.5 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -832,7 +826,7 @@ dependencies = [ [[package]] name = "itoa" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -899,7 +893,7 @@ name = "line_drawing" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -952,10 +946,10 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "adler32 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -982,7 +976,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "mio 0.6.22 (registry+https://github.com/rust-lang/crates.io-index)", - "miow 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "miow 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "spsc-buffer 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1005,7 +999,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "mio 0.6.22 (registry+https://github.com/rust-lang/crates.io-index)", - "miow 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "miow 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1022,7 +1016,7 @@ dependencies = [ [[package]] name = "miow" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "socket2 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1110,7 +1104,7 @@ dependencies = [ [[package]] name = "nom" -version = "5.1.1" +version = "5.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1136,21 +1130,21 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.42" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "num-iter" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1159,13 +1153,13 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "num-traits" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1187,8 +1181,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro-crate 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1249,7 +1243,7 @@ dependencies = [ "cc 1.0.54 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", - "vcpkg 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)", + "vcpkg 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1257,7 +1251,7 @@ name = "ordered-float" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1341,13 +1335,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "png" -version = "0.16.4" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "deflate 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)", - "inflate 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "miniz_oxide 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1399,7 +1393,7 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1486,13 +1480,22 @@ dependencies = [ ] [[package]] +name = "regex-automata" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.6.18 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "regex-syntax" version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "remove_dir_all" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1592,40 +1595,40 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.111" +version = "1.0.112" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "serde_derive 1.0.111 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.112 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "serde_derive" -version = "1.0.111" +version = "1.0.112" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "serde_json" -version = "1.0.53" +version = "1.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.111 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.112 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "serde_yaml" -version = "0.8.12" +version = "0.8.13" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "dtoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "dtoa 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "linked-hash-map 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.111 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.112 (registry+https://github.com/rust-lang/crates.io-index)", "yaml-rust 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1664,7 +1667,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "signal-hook" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1768,11 +1771,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "syn" -version = "1.0.30" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1785,7 +1788,7 @@ dependencies = [ "libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", - "remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "remove_dir_all 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1804,7 +1807,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "fnv 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "nom 5.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "nom 5.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "phf 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "phf_codegen 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1839,7 +1842,7 @@ name = "toml" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "serde 1.0.111 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.112 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1877,7 +1880,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "vcpkg" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -1928,7 +1931,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2004,7 +2007,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "nix 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", "wayland-client 0.26.6 (registry+https://github.com/rust-lang/crates.io-index)", - "xcursor 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "xcursor 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2045,7 +2048,7 @@ version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "xml-rs 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -2136,7 +2139,7 @@ dependencies = [ "parking_lot 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "raw-window-handle 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.111 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.112 (registry+https://github.com/rust-lang/crates.io-index)", "smithay-client-toolkit 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)", "wayland-client 0.23.6 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2152,7 +2155,7 @@ dependencies = [ "http_req 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "winpty-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "zip 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", + "zip 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2219,10 +2222,10 @@ dependencies = [ [[package]] name = "xcursor" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "nom 5.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "nom 5.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2245,7 +2248,7 @@ dependencies = [ [[package]] name = "zip" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bzip2 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2256,7 +2259,7 @@ dependencies = [ ] [metadata] -"checksum adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2" +"checksum adler32 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d" "checksum aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)" = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" "checksum andrew 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9b7f09f89872c2b6b29e319377b1fbe91c6f5947df19a25596e121cf19a7b35e" "checksum android_glue 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "000444226fcff248f2bc4c7625be32c63caccfecc2723a2b9f78a7487a49c407" @@ -2269,7 +2272,7 @@ dependencies = [ "checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" "checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" "checksum base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" -"checksum base64 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "53d1ccbaf7d9ec9537465a97bf19edc1a4e158ecb49fc16178202238c569cc42" +"checksum base64 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e223af0dc48c96d4f8342ec01a4974f139df863896b316681efd36742f22cc67" "checksum bindgen 0.53.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c72a978d268b1d70b0e963217e60fdabd9523a941457a6c42a7315d15c7e89e5" "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" "checksum blake2b_simd 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" @@ -2305,7 +2308,7 @@ dependencies = [ "checksum dispatch 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" "checksum dlib 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b11f15d1e3268f140f68d390637d5e76d849782d971ae7063e0da69fe9709a76" "checksum downcast-rs 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "52ba6eb47c2131e784a38b726eb54c1e1484904f013e576a25354d0124161af6" -"checksum dtoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3" +"checksum dtoa 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b" "checksum dwrote 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b" "checksum embed-resource 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1f6b0b4403da80c2fd32333937dd468292c001d778c587ae759b75432772715d" "checksum env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" @@ -2335,16 +2338,15 @@ dependencies = [ "checksum glutin_gles2_sys 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "07e853d96bebcb8e53e445225c3009758c6f5960d44f2543245f6f07b567dae0" "checksum glutin_glx_sys 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "08c243de74d6cf5ea100c788826d2fb9319de315485dd4b310811a663b3809c3" "checksum glutin_wgl_sys 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a93dba7ee3a0feeac0f437141ff25e71ce2066bcf1a706acab1559ffff94eb6a" -"checksum hermit-abi 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "91780f809e750b0a89f5544be56617ff6b1227ee485bcb06ebe10cdf89bd3b71" +"checksum hermit-abi 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "b9586eedd4ce6b3c498bc3b4dd92fc9f11166aa908a914071953768066c67909" "checksum http_req 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ef9a6b5b2cd80630d9c6bda175408a86908d8a9c1eb5b2857206529d88d063a3" "checksum humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" -"checksum image 0.23.4 (registry+https://github.com/rust-lang/crates.io-index)" = "9117f4167a8f21fa2bb3f17a652a760acd7572645281c98e3b612a26242c96ee" -"checksum inflate 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" +"checksum image 0.23.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d534e95ad8b9d5aa614322d02352b4f1bf962254adcf02ac6f2def8be18498e8" "checksum inotify 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f" "checksum inotify-sys 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e74a1aa87c59aeff6ef2cc2fa62d41bc43f54952f55652656b18a02fd5e356c0" "checksum instant 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7777a24a1ce5de49fcdde84ec46efa487c3af49d5b6e6e0a50367cc5c1096182" "checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" -"checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" +"checksum itoa 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" "checksum jni-sys 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" "checksum jobserver 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)" = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" @@ -2362,13 +2364,13 @@ dependencies = [ "checksum maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" "checksum memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" "checksum memmap 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b" -"checksum miniz_oxide 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "aa679ff6578b1cddee93d7e82e263b94a575e0bfced07284eb0c037c1d2416a5" +"checksum miniz_oxide 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" "checksum mio 0.6.22 (registry+https://github.com/rust-lang/crates.io-index)" = "fce347092656428bc8eaf6201042cb551b8d67855af7374542a92a0fbfcac430" "checksum mio-anonymous-pipes 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8c274c3c52dcd1d78c5d7ed841eca1e9ea2db8353f3b8ec25789cc62c471aaf" "checksum mio-extras 2.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" "checksum mio-named-pipes 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "f5e374eff525ce1c5b7687c4cef63943e7686524a387933ad27ca7ec43779cb3" "checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" -"checksum miow 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "22dfdd1d51b2639a5abd17ed07005c3af05fb7a2a3b1a1d0d7af1000a520c1c7" +"checksum miow 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "07b88fb9795d4d36d62a012dfbf49a8f5cf12751f36d31a9dbe66d528e58979e" "checksum native-tls 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d" "checksum ndk 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "95a356cafe20aee088789830bfea3a61336e84ded9e545e00d3869ce95dcb80c" "checksum ndk-glue 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d1730ee2e3de41c3321160a6da815f008c4006d71b095880ea50e17cf52332b8" @@ -2376,12 +2378,12 @@ dependencies = [ "checksum net2 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)" = "2ba7c918ac76704fb42afcbbb43891e72731f3dcca3bef2a19786297baf14af7" "checksum nix 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6c722bee1037d430d0f8e687bbdbf222f27cc6e4e68d5caf630857bb2b6dbdce" "checksum nix 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "50e4785f2c3b7589a0d0c1dd60285e1188adac4006e8abd6dd578e1567027363" -"checksum nom 5.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b471253da97532da4b61552249c521e01e736071f71c1a4f7ebbfbf0a06aad6" +"checksum nom 5.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" "checksum notify 4.0.15 (registry+https://github.com/rust-lang/crates.io-index)" = "80ae4a7688d1fab81c5bf19c64fc8db920be8d519ce6336ed4e7efe024724dbd" -"checksum num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" -"checksum num-iter 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "dfb0800a0291891dd9f4fe7bd9c19384f98f7fbe0cd0f39a2c6b88b9868bbc00" +"checksum num-integer 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" +"checksum num-iter 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e6b7c748f995c4c29c5f5ae0248536e04a5739927c74ec0fa564805094b9f" "checksum num-rational 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" -"checksum num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" +"checksum num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" "checksum num_enum 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ca565a7df06f3d4b485494f25ba05da1435950f4dc263440eda7a6fa9b8e36e4" "checksum num_enum_derive 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ffa5a33ddddfee04c0283a7653987d634e880347e96b5b2ed64de07efb59db9d" "checksum objc 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" @@ -2402,7 +2404,7 @@ dependencies = [ "checksum phf_generator 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" "checksum phf_shared 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" "checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" -"checksum png 0.16.4 (registry+https://github.com/rust-lang/crates.io-index)" = "12faa637ed9ae3d3c881332e54b5ae2dba81cda9fc4bbce0faa1ba53abcead50" +"checksum png 0.16.5 (registry+https://github.com/rust-lang/crates.io-index)" = "34ccdd66f6fe4b2433b07e4728e9a013e43233120427046e93ceb709c3a439bf" "checksum podio 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "b18befed8bc2b61abc79a457295e7e838417326da1586050b919414073977f19" "checksum ppv-lite86 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" "checksum proc-macro-crate 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "e10d4b51f154c8a7fb96fd6dad097cb74b863943ec010ac94b9fd1be8861fe1e" @@ -2410,7 +2412,7 @@ dependencies = [ "checksum proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)" = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa" "checksum quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" "checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" -"checksum quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "54a21852a652ad6f610c9510194f398ff6f8692e334fd1145fed931f7fbe44ea" +"checksum quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" "checksum rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" "checksum rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" "checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" @@ -2420,8 +2422,9 @@ dependencies = [ "checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" "checksum redox_users 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431" "checksum regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" +"checksum regex-automata 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4" "checksum regex-syntax 0.6.18 (registry+https://github.com/rust-lang/crates.io-index)" = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" -"checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" +"checksum remove_dir_all 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" "checksum rust-argon2 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017" "checksum rustc-hash 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" "checksum rustc_tools_util 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b725dadae9fabc488df69a287f5a99c5eaf5d10853842a8a3dfac52476f544ee" @@ -2434,15 +2437,15 @@ dependencies = [ "checksum scopeguard 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" "checksum security-framework 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535" "checksum security-framework-sys 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405" -"checksum serde 1.0.111 (registry+https://github.com/rust-lang/crates.io-index)" = "c9124df5b40cbd380080b2cc6ab894c040a3070d995f5c9dc77e18c34a8ae37d" -"checksum serde_derive 1.0.111 (registry+https://github.com/rust-lang/crates.io-index)" = "3f2c3ac8e6ca1e9c80b8be1023940162bf81ae3cffbb1809474152f2ce1eb250" -"checksum serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)" = "993948e75b189211a9b31a7528f950c6adc21f9720b6438ff80a7fa2f864cea2" -"checksum serde_yaml 0.8.12 (registry+https://github.com/rust-lang/crates.io-index)" = "16c7a592a1ec97c9c1c68d75b6e537dcbf60c7618e038e7841e00af1d9ccf0c4" +"checksum serde 1.0.112 (registry+https://github.com/rust-lang/crates.io-index)" = "736aac72d1eafe8e5962d1d1c3d99b0df526015ba40915cb3c49d042e92ec243" +"checksum serde_derive 1.0.112 (registry+https://github.com/rust-lang/crates.io-index)" = "bf0343ce212ac0d3d6afd9391ac8e9c9efe06b533c8d33f660f6390cc4093f57" +"checksum serde_json 1.0.55 (registry+https://github.com/rust-lang/crates.io-index)" = "ec2c5d7e739bc07a3e73381a39d61fdb5f671c60c1df26a130690665803d8226" +"checksum serde_yaml 0.8.13 (registry+https://github.com/rust-lang/crates.io-index)" = "ae3e2dd40a7cdc18ca80db804b7f461a39bb721160a85c9a1fa30134bf3c02a5" "checksum servo-fontconfig 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0b47fef69c52fb55838c756949c60595f0b855daa4e82fc52ad99ff3e03e2c70" "checksum servo-fontconfig-sys 5.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "facb23c6a801c935c3bddfdd7dc4e823af853babc5b0c90ffa3419ebef5d92c7" "checksum shared_library 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" "checksum shlex 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" -"checksum signal-hook 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "8ff2db2112d6c761e12522c65f7768548bd6e8cd23d2a9dae162520626629bd6" +"checksum signal-hook 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "604508c1418b99dfe1925ca9224829bb2a8a9a04dda655cc01fcad46f4ab05ed" "checksum signal-hook-registry 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "94f478ede9f64724c5d173d7bb56099ec3e2d9fc2774aac65d34b8b890405f41" "checksum siphasher 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "fa8f3741c7372e75519bd9346068370c9cdaabcc1f9599cbcf2a2719352286b7" "checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" @@ -2454,7 +2457,7 @@ dependencies = [ "checksum spsc-buffer 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "be6c3f39c37a4283ee4b43d1311c828f2e1fb0541e76ea0cb1a2abd9ef2f5b3b" "checksum stb_truetype 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f77b6b07e862c66a9f3e62a07588fee67cd90a9135a2b942409f195507b4fb51" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" -"checksum syn 1.0.30 (registry+https://github.com/rust-lang/crates.io-index)" = "93a56fabc59dce20fe48b6c832cc249c713e7ed88fa28b0ee0a3bfcaae5fe4e2" +"checksum syn 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)" = "b5304cfdf27365b7585c25d4af91b35016ed21ef88f17ced89c7093b43dba8b6" "checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" "checksum termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" "checksum terminfo 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "76971977e6121664ec1b960d1313aacfa75642adc93b9d4d53b247bd4cb1747e" @@ -2468,7 +2471,7 @@ dependencies = [ "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" "checksum urlocator 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "317bb1e85e87e72c11cb9cda7cb8909ca174a21d0abeb0f6955b8f6b0178b164" "checksum utf8parse 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" -"checksum vcpkg 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)" = "55d1e41d56121e07f1e223db0a4def204e45c85425f6a16d462fd07c8d10d74c" +"checksum vcpkg 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" "checksum vec_map 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" "checksum version_check 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" @@ -2505,8 +2508,8 @@ dependencies = [ "checksum x11-clipboard 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e5e937afd03b64b7be4f959cc044e09260a47241b71e56933f37db097bf7859d" "checksum x11-dl 2.18.5 (registry+https://github.com/rust-lang/crates.io-index)" = "2bf981e3a5b3301209754218f962052d4d9ee97e478f4d26d4a6eced34c1fef8" "checksum xcb 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "62056f63138b39116f82a540c983cc11f1c90cd70b3d492a70c25eaa50bd22a6" -"checksum xcursor 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "56edec90b342d0f46d1189f3fd4a3374954636f3d76c861855b675efc7ef1435" +"checksum xcursor 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d3a481cfdefd35e1c50073ae33a8000d695c98039544659f5dc5dd71311b0d01" "checksum xdg 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57" "checksum xml-rs 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b07db065a5cf61a7e4ba64f29e67db906fb1787316516c4e6e5ff0fea1efcd8a" "checksum yaml-rust 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "39f0c922f1a334134dc2f7a8b67dc5d25f0735263feec974345ff706bcf20b0d" -"checksum zip 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6df134e83b8f0f8153a094c7b0fd79dfebe437f1d76e7715afa18ed95ebe2fd7" +"checksum zip 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "58287c28d78507f5f91f2a4cf1e8310e2c76fd4c6932f93ac60fd1ceb402db7d" diff --git a/alacritty.yml b/alacritty.yml index 2fb3b59f..ed6feafc 100644 --- a/alacritty.yml +++ b/alacritty.yml @@ -176,28 +176,48 @@ # Cursor colors # - # Colors which should be used to draw the terminal cursor. If these are - # unset, the cursor color will be the inverse of the cell color. + # Colors which should be used to draw the terminal cursor. + # + # Allowed values are CellForeground and CellBackground, which reference the + # affected cell, or hexadecimal colors like #ff00ff. #cursor: - # text: '#000000' - # cursor: '#ffffff' + # text: CellBackground + # cursor: CellForeground # Vi mode cursor colors # - # Colors for the cursor when the vi mode is active. If these are unset, the - # cursor color will be the inverse of the cell color. + # Colors for the cursor when the vi mode is active. + # + # Allowed values are CellForeground and CellBackground, which reference the + # affected cell, or hexadecimal colors like #ff00ff. #vi_mode_cursor: - # text: '#000000' - # cursor: '#ffffff' + # text: CellBackground + # cursor: CellForeground # Selection colors # - # Colors which should be used to draw the selection area. If selection - # background is unset, selection color will be the inverse of the cell colors. - # If only text is unset the cell text color will remain the same. + # Colors which should be used to draw the selection area. + # + # Allowed values are CellForeground and CellBackground, which reference the + # affected cell, or hexadecimal colors like #ff00ff. #selection: - # text: '#eaeaea' - # background: '#404040' + # text: CellBackground + # background: CellForeground + + # Search colors + # + # Colors used for the search bar and match highlighting. + # + # Allowed values are CellForeground and CellBackground, which reference the + # affected cell, or hexadecimal colors like #ff00ff. + #search: + # matches: + # foreground: '#000000' + # background: '#ffffff' + # + # bar: + # background: CellForeground + # foreground: CellBackground # Normal colors #normal: @@ -445,6 +465,8 @@ # - `action`: Execute a predefined action # # - ToggleViMode +# - Search +# - SearchReverse # - Copy # - Paste # - PasteSelection @@ -495,6 +517,10 @@ # - ToggleLineSelection # - ToggleBlockSelection # - ToggleSemanticSelection +# - SearchNext +# - SearchPrevious +# - SearchEndNext +# - SearchEndPrevious # # (macOS only): # - ToggleSimpleFullscreen: Enters fullscreen without occupying another space @@ -595,6 +621,10 @@ #- { key: W, mods: Shift, mode: Vi, action: WordRight } #- { key: E, mods: Shift, mode: Vi, action: WordRightEnd } #- { key: Key5, mods: Shift, mode: Vi, action: Bracket } + #- { key: Slash, mode: Vi, action: Search } + #- { key: Slash, mods: Shift, mode: Vi, action: SearchReverse } + #- { key: N, mode: Vi, action: SearchNext } + #- { key: N, mods: Shift, mode: Vi, action: SearchPrevious } # (Windows, Linux, and BSD only) #- { key: V, mods: Control|Shift, action: Paste } diff --git a/alacritty/Cargo.toml b/alacritty/Cargo.toml index 184f2102..62942985 100644 --- a/alacritty/Cargo.toml +++ b/alacritty/Cargo.toml @@ -23,6 +23,7 @@ parking_lot = "0.10.2" font = { path = "../font" } urlocator = "0.1.3" copypasta = { version = "0.7.0", default-features = false } +unicode-width = "0.1" [build-dependencies] gl_generator = "0.14.0" diff --git a/alacritty/src/config/bindings.rs b/alacritty/src/config/bindings.rs index 547e168c..81a46d66 100644 --- a/alacritty/src/config/bindings.rs +++ b/alacritty/src/config/bindings.rs @@ -176,6 +176,12 @@ pub enum Action { /// Allow receiving char input. ReceiveChar, + /// Start a buffer search. + Search, + + /// Start a reverse buffer search. + SearchReverse, + /// No action. None, } @@ -208,6 +214,14 @@ pub enum ViAction { ToggleBlockSelection, /// Toggle semantic vi selection. ToggleSemanticSelection, + /// Jump to the beginning of the next match. + SearchNext, + /// Jump to the beginning of the previous match. + SearchPrevious, + /// Jump to the end of the next match. + SearchEndNext, + /// Jump to the end of the previous match. + SearchEndPrevious, /// Launch the URL below the vi mode cursor. Open, } @@ -364,10 +378,14 @@ pub fn default_key_bindings() -> Vec<KeyBinding> { D, ModifiersState::CTRL, +TermMode::VI; Action::ScrollHalfPageDown; Y, +TermMode::VI; Action::Copy; Y, +TermMode::VI; Action::ClearSelection; + Slash, +TermMode::VI; Action::Search; + Slash, ModifiersState::SHIFT, +TermMode::VI; Action::SearchReverse; V, +TermMode::VI; ViAction::ToggleNormalSelection; V, ModifiersState::SHIFT, +TermMode::VI; ViAction::ToggleLineSelection; V, ModifiersState::CTRL, +TermMode::VI; ViAction::ToggleBlockSelection; V, ModifiersState::ALT, +TermMode::VI; ViAction::ToggleSemanticSelection; + N, +TermMode::VI; ViAction::SearchNext; + N, ModifiersState::SHIFT, +TermMode::VI; ViAction::SearchPrevious; Return, +TermMode::VI; ViAction::Open; K, +TermMode::VI; ViMotion::Up; J, +TermMode::VI; ViMotion::Down; diff --git a/alacritty/src/display.rs b/alacritty/src/display.rs index d61a5bbd..53e6fc58 100644 --- a/alacritty/src/display.rs +++ b/alacritty/src/display.rs @@ -1,6 +1,7 @@ //! The display subsystem including window management, font rasterization, and //! GPU drawing. +use std::cmp::min; use std::f64; use std::fmt::{self, Formatter}; #[cfg(not(any(target_os = "macos", windows)))] @@ -15,6 +16,7 @@ use glutin::platform::unix::EventLoopWindowTargetExtUnix; use glutin::window::CursorIcon; use log::{debug, info}; use parking_lot::MutexGuard; +use unicode_width::UnicodeWidthChar; #[cfg(not(any(target_os = "macos", windows)))] use wayland_client::{Display as WaylandDisplay, EventQueue}; @@ -23,21 +25,23 @@ use font::set_font_smoothing; use font::{self, Rasterize}; use alacritty_terminal::config::{Font, StartupMode}; -use alacritty_terminal::event::OnResize; -use alacritty_terminal::index::Line; +use alacritty_terminal::event::{EventListener, OnResize}; +use alacritty_terminal::grid::Dimensions; +use alacritty_terminal::index::{Column, Line, Point}; use alacritty_terminal::message_bar::MessageBuffer; use alacritty_terminal::meter::Meter; use alacritty_terminal::selection::Selection; -use alacritty_terminal::term::color::Rgb; use alacritty_terminal::term::{RenderableCell, SizeInfo, Term, TermMode}; use crate::config::Config; -use crate::event::{DisplayUpdate, Mouse}; +use crate::event::Mouse; use crate::renderer::rects::{RenderLines, RenderRect}; use crate::renderer::{self, GlyphCache, QuadRenderer}; use crate::url::{Url, Urls}; use crate::window::{self, Window}; +const SEARCH_LABEL: &str = "Search: "; + #[derive(Debug)] pub enum Error { /// Error with window management. @@ -99,6 +103,44 @@ impl From<glutin::ContextError> for Error { } } +#[derive(Default, Clone, Debug, PartialEq)] +pub struct DisplayUpdate { + pub dirty: bool, + + dimensions: Option<PhysicalSize<u32>>, + font: Option<Font>, + cursor_dirty: bool, +} + +impl DisplayUpdate { + pub fn dimensions(&self) -> Option<PhysicalSize<u32>> { + self.dimensions + } + + pub fn font(&self) -> Option<&Font> { + self.font.as_ref() + } + + pub fn cursor_dirty(&self) -> bool { + self.cursor_dirty + } + + pub fn set_dimensions(&mut self, dimensions: PhysicalSize<u32>) { + self.dimensions = Some(dimensions); + self.dirty = true; + } + + pub fn set_font(&mut self, font: Font) { + self.font = Some(font); + self.dirty = true; + } + + pub fn set_cursor_dirty(&mut self) { + self.cursor_dirty = true; + self.dirty = true; + } +} + /// The display wraps a window, font rasterizer, and GPU renderer. pub struct Display { pub size_info: SizeInfo, @@ -300,7 +342,7 @@ impl Display { } /// Update font size and cell dimensions. - fn update_glyph_cache(&mut self, config: &Config, font: Font) { + fn update_glyph_cache(&mut self, config: &Config, font: &Font) { let size_info = &mut self.size_info; let cache = &mut self.glyph_cache; @@ -328,13 +370,16 @@ impl Display { terminal: &mut Term<T>, pty_resize_handle: &mut dyn OnResize, message_buffer: &MessageBuffer, + search_active: bool, config: &Config, update_pending: DisplayUpdate, - ) { + ) where + T: EventListener, + { // Update font size and cell dimensions. - if let Some(font) = update_pending.font { + if let Some(font) = update_pending.font() { self.update_glyph_cache(config, font); - } else if update_pending.cursor { + } else if update_pending.cursor_dirty() { self.clear_glyph_cache(); } @@ -346,7 +391,7 @@ impl Display { let mut padding_y = f32::from(config.window.padding.y) * self.size_info.dpr as f32; // Update the window dimensions. - if let Some(size) = update_pending.dimensions { + if let Some(size) = update_pending.dimensions() { // Ensure we have at least one column and row. self.size_info.width = (size.width as f32).max(cell_width + 2. * padding_x); self.size_info.height = (size.height as f32).max(cell_height + 2. * padding_y); @@ -369,6 +414,11 @@ impl Display { pty_size.height -= pty_size.cell_height * lines as f32; } + // Add an extra line for the current search regex. + if search_active { + pty_size.height -= pty_size.cell_height; + } + // Resize PTY. pty_resize_handle.on_resize(&pty_size); @@ -393,8 +443,10 @@ impl Display { config: &Config, mouse: &Mouse, mods: ModifiersState, + search_regex: Option<&String>, ) { let grid_cells: Vec<RenderableCell> = terminal.renderable_cells(config).collect(); + let search_regex = search_regex.map(|regex| Self::format_search(®ex)); let visual_bell_intensity = terminal.visual_bell.intensity(); let background_color = terminal.background_color(); let metrics = self.glyph_cache.font_metrics(); @@ -413,7 +465,17 @@ impl Display { // Update IME position. #[cfg(not(windows))] - self.window.update_ime_position(&terminal, &self.size_info); + { + let point = match &search_regex { + Some(regex) => { + let column = min(regex.len() + SEARCH_LABEL.len() - 1, terminal.cols().0 - 1); + Point::new(terminal.screen_lines() - 1, Column(column)) + }, + None => terminal.grid().cursor.point, + }; + + self.window.update_ime_position(point, &self.size_info); + } // Drop terminal as early as possible to free lock. drop(terminal); @@ -484,11 +546,13 @@ impl Display { rects.push(visual_bell_rect); } + let mut message_bar_lines = 0; if let Some(message) = message_buffer.message() { let text = message.text(&size_info); + message_bar_lines = text.len(); // Create a new rectangle for the background. - let start_line = size_info.lines().0 - text.len(); + let start_line = size_info.lines().0 - message_bar_lines; let y = size_info.cell_height.mul_add(start_line as f32, size_info.padding_y); let message_bar_rect = RenderRect::new(0., y, size_info.width, size_info.height - y, message.color(), 1.); @@ -500,31 +564,25 @@ impl Display { self.renderer.draw_rects(&size_info, rects); // Relay messages to the user. - let mut offset = 1; - for message_text in text.iter().rev() { + let fg = config.colors.primary.background; + for (i, message_text) in text.iter().rev().enumerate() { self.renderer.with_api(&config, &size_info, |mut api| { api.render_string( - &message_text, - Line(size_info.lines().saturating_sub(offset)), glyph_cache, + Line(size_info.lines().saturating_sub(i + 1)), + &message_text, + fg, None, ); }); - offset += 1; } } else { // Draw rectangles. self.renderer.draw_rects(&size_info, rects); } - // Draw render timer. - if config.render_timer() { - let timing = format!("{:.3} usec", self.meter.average()); - let color = Rgb { r: 0xd5, g: 0x4e, b: 0x53 }; - self.renderer.with_api(&config, &size_info, |mut api| { - api.render_string(&timing[..], size_info.lines() - 2, glyph_cache, Some(color)); - }); - } + self.draw_search(config, &size_info, message_bar_lines, search_regex); + self.draw_render_timer(config, &size_info); // Frame event should be requested before swaping buffers, since it requires surface // `commit`, which is done by swap buffers under the hood. @@ -546,6 +604,73 @@ impl Display { } } + /// Format search regex to account for the cursor and fullwidth characters. + fn format_search(search_regex: &str) -> String { + // Add spacers for wide chars. + let mut text = String::with_capacity(search_regex.len()); + for c in search_regex.chars() { + text.push(c); + if c.width() == Some(2) { + text.push(' '); + } + } + + // Add cursor to show whitespace. + text.push('_'); + + text + } + + /// Draw current search regex. + fn draw_search( + &mut self, + config: &Config, + size_info: &SizeInfo, + message_bar_lines: usize, + search_regex: Option<String>, + ) { + let search_regex = match search_regex { + Some(search_regex) => search_regex, + None => return, + }; + let glyph_cache = &mut self.glyph_cache; + + let label_len = SEARCH_LABEL.len(); + let num_cols = size_info.cols().0; + + // Truncate beginning of text when it exceeds viewport width. + let text_len = search_regex.len(); + let truncate_len = min((text_len + label_len).saturating_sub(num_cols), text_len); + let text = &search_regex[truncate_len..]; + + // Assure text length is at least num_cols. + let padding_len = num_cols.saturating_sub(label_len); + let text = format!("{}{:<2$}", SEARCH_LABEL, text, padding_len); + + let fg = config.colors.search_bar_foreground(); + let bg = config.colors.search_bar_background(); + let line = size_info.lines() - message_bar_lines - 1; + self.renderer.with_api(&config, &size_info, |mut api| { + api.render_string(glyph_cache, line, &text, fg, Some(bg)); + }); + } + + /// Draw render timer. + fn draw_render_timer(&mut self, config: &Config, size_info: &SizeInfo) { + if !config.render_timer() { + return; + } + let glyph_cache = &mut self.glyph_cache; + + let timing = format!("{:.3} usec", self.meter.average()); + let fg = config.colors.normal().black; + let bg = config.colors.normal().red; + + self.renderer.with_api(&config, &size_info, |mut api| { + api.render_string(glyph_cache, size_info.lines() - 2, &timing[..], fg, Some(bg)); + }); + } + /// Requst a new frame for a window on Wayland. #[inline] #[cfg(not(any(target_os = "macos", windows)))] diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs index 630e8ef0..edbc4086 100644 --- a/alacritty/src/event.rs +++ b/alacritty/src/event.rs @@ -1,7 +1,7 @@ //! Process window events. use std::borrow::Cow; -use std::cmp::max; +use std::cmp::{max, min}; use std::env; #[cfg(unix)] use std::fs; @@ -12,7 +12,7 @@ use std::path::PathBuf; #[cfg(not(any(target_os = "macos", windows)))] use std::sync::atomic::Ordering; use std::sync::Arc; -use std::time::Instant; +use std::time::{Duration, Instant}; use glutin::dpi::PhysicalSize; use glutin::event::{ElementState, Event as GlutinEvent, ModifiersState, MouseButton, WindowEvent}; @@ -27,12 +27,10 @@ use serde_json as json; use font::set_font_smoothing; use font::{self, Size}; -use alacritty_terminal::config::Font; use alacritty_terminal::config::LOG_TARGET_CONFIG; -use alacritty_terminal::event::OnResize; -use alacritty_terminal::event::{Event as TerminalEvent, EventListener, Notify}; -use alacritty_terminal::grid::Scroll; -use alacritty_terminal::index::{Column, Line, Point, Side}; +use alacritty_terminal::event::{Event as TerminalEvent, EventListener, Notify, OnResize}; +use alacritty_terminal::grid::{Dimensions, Scroll}; +use alacritty_terminal::index::{Column, Direction, Line, Point, Side}; use alacritty_terminal::message_bar::{Message, MessageBuffer}; use alacritty_terminal::selection::{Selection, SelectionType}; use alacritty_terminal::sync::FairMutex; @@ -46,12 +44,18 @@ use crate::cli::Options; use crate::clipboard::Clipboard; use crate::config; use crate::config::Config; -use crate::display::Display; +use crate::display::{Display, DisplayUpdate}; use crate::input::{self, ActionContext as _, FONT_SIZE_STEP}; -use crate::scheduler::Scheduler; +use crate::scheduler::{Scheduler, TimerId}; use crate::url::{Url, Urls}; use crate::window::Window; +/// Duration after the last user input until an unlimited search is performed. +pub const TYPING_SEARCH_DELAY: Duration = Duration::from_millis(500); + +/// Maximum number of lines for the blocking search while still typing the search regex. +const MAX_SEARCH_WHILE_TYPING: Option<usize> = Some(1000); + /// Events dispatched through the UI event loop. #[derive(Debug, Clone)] pub enum Event { @@ -60,6 +64,7 @@ pub enum Event { Scroll(Scroll), ConfigReload(PathBuf), Message(Message), + SearchNext, } impl From<Event> for GlutinEvent<'_, Event> { @@ -74,17 +79,35 @@ impl From<TerminalEvent> for Event { } } -#[derive(Default, Clone, Debug, PartialEq)] -pub struct DisplayUpdate { - pub dimensions: Option<PhysicalSize<u32>>, - pub message_buffer: bool, - pub font: Option<Font>, - pub cursor: bool, +/// Regex search state. +pub struct SearchState { + /// Search string regex. + regex: Option<String>, + + /// Search direction. + direction: Direction, + + /// Change in display offset since the beginning of the search. + display_offset_delta: isize, + + /// Vi cursor position before search. + vi_cursor_point: Point, } -impl DisplayUpdate { - fn is_empty(&self) -> bool { - self.dimensions.is_none() && self.font.is_none() && !self.message_buffer && !self.cursor +impl SearchState { + fn new() -> Self { + Self::default() + } +} + +impl Default for SearchState { + fn default() -> Self { + Self { + direction: Direction::Right, + display_offset_delta: 0, + vi_cursor_point: Point::default(), + regex: None, + } } } @@ -104,6 +127,7 @@ pub struct ActionContext<'a, N, T> { pub event_loop: &'a EventLoopWindowTarget<Event>, pub urls: &'a Urls, pub scheduler: &'a mut Scheduler, + pub search_state: &'a mut SearchState, font_size: &'a mut Size, } @@ -294,19 +318,148 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon fn change_font_size(&mut self, delta: f32) { *self.font_size = max(*self.font_size + delta, Size::new(FONT_SIZE_STEP)); let font = self.config.font.clone().with_size(*self.font_size); - self.display_update_pending.font = Some(font); + self.display_update_pending.set_font(font); self.terminal.dirty = true; } fn reset_font_size(&mut self) { *self.font_size = self.config.font.size; - self.display_update_pending.font = Some(self.config.font.clone()); + self.display_update_pending.set_font(self.config.font.clone()); self.terminal.dirty = true; } + #[inline] fn pop_message(&mut self) { - self.display_update_pending.message_buffer = true; - self.message_buffer.pop(); + if !self.message_buffer.is_empty() { + self.display_update_pending.dirty = true; + self.message_buffer.pop(); + } + } + + #[inline] + fn start_search(&mut self, direction: Direction) { + let num_lines = self.terminal.screen_lines(); + let num_cols = self.terminal.cols(); + + self.search_state.regex = Some(String::new()); + self.search_state.direction = direction; + + // Store original vi cursor position as search origin and for resetting. + self.search_state.vi_cursor_point = if self.terminal.mode().contains(TermMode::VI) { + self.terminal.vi_mode_cursor.point + } else { + match direction { + Direction::Right => Point::new(Line(0), Column(0)), + Direction::Left => Point::new(num_lines - 2, num_cols - 1), + } + }; + + self.display_update_pending.dirty = true; + self.terminal.dirty = true; + } + + #[inline] + fn confirm_search(&mut self) { + // Enter vi mode once search is confirmed. + self.terminal.set_vi_mode(); + + // Force unlimited search if the previous one was interrupted. + if self.scheduler.scheduled(TimerId::DelayedSearch) { + self.goto_match(None); + } + + // Move vi cursor down if resize will pull content from history. + if self.terminal.history_size() != 0 && self.terminal.grid().display_offset() == 0 { + self.terminal.vi_mode_cursor.point.line += 1; + } + + // Clear reset state. + self.search_state.display_offset_delta = 0; + + self.display_update_pending.dirty = true; + self.search_state.regex = None; + self.terminal.dirty = true; + } + + #[inline] + fn cancel_search(&mut self) { + self.terminal.cancel_search(); + + // Recover pre-search state. + self.search_reset_state(); + + // Move vi cursor down if resize will pull from history. + if self.terminal.history_size() != 0 && self.terminal.grid().display_offset() == 0 { + self.terminal.vi_mode_cursor.point.line += 1; + } + + self.display_update_pending.dirty = true; + self.search_state.regex = None; + self.terminal.dirty = true; + } + + #[inline] + fn push_search(&mut self, c: char) { + let regex = match self.search_state.regex.as_mut() { + Some(regex) => regex, + None => return, + }; + + // Hide cursor while typing into the search bar. + if self.config.ui_config.mouse.hide_when_typing { + self.window.set_mouse_visible(false); + } + + // Add new char to search string. + regex.push(c); + + // Create terminal search from the new regex string. + self.terminal.start_search(®ex); + + // Update search highlighting. + self.goto_match(MAX_SEARCH_WHILE_TYPING); + + self.terminal.dirty = true; + } + + #[inline] + fn pop_search(&mut self) { + let regex = match self.search_state.regex.as_mut() { + Some(regex) => regex, + None => return, + }; + + // Hide cursor while typing into the search bar. + if self.config.ui_config.mouse.hide_when_typing { + self.window.set_mouse_visible(false); + } + + // Remove last char from search string. + regex.pop(); + + if regex.is_empty() { + // Stop search if there's nothing to search for. + self.search_reset_state(); + self.terminal.cancel_search(); + } else { + // Create terminal search from the new regex string. + self.terminal.start_search(®ex); + + // Update search highlighting. + self.goto_match(MAX_SEARCH_WHILE_TYPING); + } + + self.terminal.dirty = true; + } + + #[inline] + fn search_direction(&self) -> Direction { + self.search_state.direction + } + + #[inline] + fn search_active(&self) -> bool { + self.search_state.regex.is_some() } fn message(&self) -> Option<&Message> { @@ -334,6 +487,73 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon } } +impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { + /// Reset terminal to the state before search was started. + fn search_reset_state(&mut self) { + // Reset display offset. + self.terminal.scroll_display(Scroll::Delta(self.search_state.display_offset_delta)); + self.search_state.display_offset_delta = 0; + + // Reset vi mode cursor. + let mut vi_cursor_point = self.search_state.vi_cursor_point; + vi_cursor_point.line = min(vi_cursor_point.line, self.terminal.screen_lines() - 1); + vi_cursor_point.col = min(vi_cursor_point.col, self.terminal.cols() - 1); + self.terminal.vi_mode_cursor.point = vi_cursor_point; + + // Unschedule pending timers. + self.scheduler.unschedule(TimerId::DelayedSearch); + } + + /// Jump to the first regex match from the search origin. + fn goto_match(&mut self, mut limit: Option<usize>) { + let regex = match self.search_state.regex.take() { + Some(regex) => regex, + None => return, + }; + + // Limit search only when enough lines are available to run into the limit. + limit = limit.filter(|&limit| limit <= self.terminal.total_lines()); + + // Use original position as search origin. + let mut vi_cursor_point = self.search_state.vi_cursor_point; + vi_cursor_point.line = min(vi_cursor_point.line, self.terminal.screen_lines() - 1); + let mut origin = self.terminal.visible_to_buffer(vi_cursor_point); + origin.line = (origin.line as isize + self.search_state.display_offset_delta) as usize; + + // Jump to the next match. + let direction = self.search_state.direction; + match self.terminal.search_next(origin, direction, Side::Left, limit) { + Some(regex_match) => { + let old_offset = self.terminal.grid().display_offset() as isize; + + self.terminal.vi_goto_point(*regex_match.start()); + + // Store number of lines the viewport had to be moved. + let display_offset = self.terminal.grid().display_offset(); + self.search_state.display_offset_delta += old_offset - display_offset as isize; + + // Since we found a result, we require no delayed re-search. + self.scheduler.unschedule(TimerId::DelayedSearch); + }, + // Reset viewport only when we know there is no match, to prevent unnecessary jumping. + None if limit.is_none() => self.search_reset_state(), + None => { + // Schedule delayed search if we ran into our search limit. + if !self.scheduler.scheduled(TimerId::DelayedSearch) { + self.scheduler.schedule( + Event::SearchNext.into(), + TYPING_SEARCH_DELAY, + false, + TimerId::DelayedSearch, + ); + } + }, + } + + self.search_state.regex = Some(regex); + } +} + #[derive(Debug, Eq, PartialEq)] pub enum ClickState { None, @@ -400,6 +620,7 @@ pub struct Processor<N> { display: Display, font_size: Size, event_queue: Vec<GlutinEvent<'static, Event>>, + search_state: SearchState, } impl<N: Notify + OnResize> Processor<N> { @@ -429,6 +650,7 @@ impl<N: Notify + OnResize> Processor<N> { display, event_queue: Vec::new(), clipboard, + search_state: SearchState::new(), } } @@ -513,6 +735,7 @@ impl<N: Notify + OnResize> Processor<N> { let mut terminal = terminal.lock(); let mut display_update_pending = DisplayUpdate::default(); + let old_is_searching = self.search_state.regex.is_some(); let context = ActionContext { terminal: &mut terminal, @@ -530,6 +753,7 @@ impl<N: Notify + OnResize> Processor<N> { config: &mut self.config, urls: &self.display.urls, scheduler: &mut scheduler, + search_state: &mut self.search_state, event_loop, }; let mut processor = input::Processor::new(context, &self.display.highlighted_url); @@ -539,14 +763,8 @@ impl<N: Notify + OnResize> Processor<N> { } // Process DisplayUpdate events. - if !display_update_pending.is_empty() { - self.display.handle_update( - &mut terminal, - &mut self.notifier, - &self.message_buffer, - &self.config, - display_update_pending, - ); + if display_update_pending.dirty { + self.submit_display_update(&mut terminal, old_is_searching, display_update_pending); } #[cfg(not(any(target_os = "macos", windows)))] @@ -575,12 +793,15 @@ impl<N: Notify + OnResize> Processor<N> { &self.config, &self.mouse, self.modifiers, + self.search_state.regex.as_ref(), ); } }); // Write ref tests to disk. - self.write_ref_test_results(&terminal.lock()); + if self.config.debug.ref_test { + self.write_ref_test_results(&terminal.lock()); + } } /// Handle events from glutin. @@ -598,20 +819,21 @@ impl<N: Notify + OnResize> Processor<N> { let display_update_pending = &mut processor.ctx.display_update_pending; // Push current font to update its DPR. - display_update_pending.font = - Some(processor.ctx.config.font.clone().with_size(*processor.ctx.font_size)); + let font = processor.ctx.config.font.clone(); + display_update_pending.set_font(font.with_size(*processor.ctx.font_size)); // Resize to event's dimensions, since no resize event is emitted on Wayland. - display_update_pending.dimensions = Some(PhysicalSize::new(width, height)); + display_update_pending.set_dimensions(PhysicalSize::new(width, height)); processor.ctx.size_info.dpr = scale_factor; processor.ctx.terminal.dirty = true; }, Event::Message(message) => { processor.ctx.message_buffer.push(message); - processor.ctx.display_update_pending.message_buffer = true; + processor.ctx.display_update_pending.dirty = true; processor.ctx.terminal.dirty = true; }, + Event::SearchNext => processor.ctx.goto_match(None), Event::ConfigReload(path) => Self::reload_config(&path, processor), Event::Scroll(scroll) => processor.ctx.scroll(scroll), Event::TerminalEvent(event) => match event { @@ -647,7 +869,7 @@ impl<N: Notify + OnResize> Processor<N> { } } - processor.ctx.display_update_pending.dimensions = Some(size); + processor.ctx.display_update_pending.set_dimensions(size); processor.ctx.terminal.dirty = true; }, WindowEvent::KeyboardInput { input, is_synthetic: false, .. } => { @@ -741,14 +963,14 @@ impl<N: Notify + OnResize> Processor<N> { } } - pub fn reload_config<T>( - path: &PathBuf, - processor: &mut input::Processor<T, ActionContext<N, T>>, - ) where + fn reload_config<T>(path: &PathBuf, processor: &mut input::Processor<T, ActionContext<N, T>>) + where T: EventListener, { - processor.ctx.message_buffer.remove_target(LOG_TARGET_CONFIG); - processor.ctx.display_update_pending.message_buffer = true; + if !processor.ctx.message_buffer.is_empty() { + processor.ctx.message_buffer.remove_target(LOG_TARGET_CONFIG); + processor.ctx.display_update_pending.dirty = true; + } let config = match config::reload_from(&path) { Ok(config) => config, @@ -764,7 +986,7 @@ impl<N: Notify + OnResize> Processor<N> { if (processor.ctx.config.cursor.thickness() - config.cursor.thickness()).abs() > std::f64::EPSILON { - processor.ctx.display_update_pending.cursor = true; + processor.ctx.display_update_pending.set_cursor_dirty(); } if processor.ctx.config.font != config.font { @@ -774,7 +996,7 @@ impl<N: Notify + OnResize> Processor<N> { } let font = config.font.clone().with_size(*processor.ctx.font_size); - processor.ctx.display_update_pending.font = Some(font); + processor.ctx.display_update_pending.set_font(font); } #[cfg(not(any(target_os = "macos", windows)))] @@ -793,12 +1015,44 @@ impl<N: Notify + OnResize> Processor<N> { processor.ctx.terminal.dirty = true; } - // Write the ref test results to the disk. - pub fn write_ref_test_results<T>(&self, terminal: &Term<T>) { - if !self.config.debug.ref_test { - return; + /// Submit the pending changes to the `Display`. + fn submit_display_update<T>( + &mut self, + terminal: &mut Term<T>, + old_is_searching: bool, + display_update_pending: DisplayUpdate, + ) where + T: EventListener, + { + // Compute cursor positions before resize. + let num_lines = terminal.screen_lines(); + let cursor_at_bottom = terminal.grid().cursor.point.line + 1 == num_lines; + let origin_at_bottom = (!terminal.mode().contains(TermMode::VI) + && self.search_state.direction == Direction::Left) + || terminal.vi_mode_cursor.point.line == num_lines - 1; + + self.display.handle_update( + terminal, + &mut self.notifier, + &self.message_buffer, + self.search_state.regex.is_some(), + &self.config, + display_update_pending, + ); + + // Scroll to make sure search origin is visible and content moves as little as possible. + if !old_is_searching && self.search_state.regex.is_some() { + let display_offset = terminal.grid().display_offset(); + if display_offset == 0 && cursor_at_bottom && !origin_at_bottom { + terminal.scroll_display(Scroll::Delta(1)); + } else if display_offset != 0 && origin_at_bottom { + terminal.scroll_display(Scroll::Delta(-1)); + } } + } + /// Write the ref test results to the disk. + fn write_ref_test_results<T>(&self, terminal: &Term<T>) { // Dump grid state. let mut grid = terminal.grid().clone(); grid.initialize_all(Cell::default()); diff --git a/alacritty/src/input.rs b/alacritty/src/input.rs index 2819c850..48450b12 100644 --- a/alacritty/src/input.rs +++ b/alacritty/src/input.rs @@ -15,6 +15,7 @@ use log::{debug, trace, warn}; use glutin::dpi::PhysicalPosition; use glutin::event::{ ElementState, KeyboardInput, ModifiersState, MouseButton, MouseScrollDelta, TouchPhase, + VirtualKeyCode, }; use glutin::event_loop::EventLoopWindowTarget; #[cfg(target_os = "macos")] @@ -23,8 +24,8 @@ use glutin::window::CursorIcon; use alacritty_terminal::ansi::{ClearMode, Handler}; use alacritty_terminal::event::EventListener; -use alacritty_terminal::grid::Scroll; -use alacritty_terminal::index::{Column, Line, Point, Side}; +use alacritty_terminal::grid::{Dimensions, Scroll}; +use alacritty_terminal::index::{Column, Direction, Line, Point, Side}; use alacritty_terminal::message_bar::{self, Message}; use alacritty_terminal::selection::SelectionType; use alacritty_terminal::term::mode::TermMode; @@ -34,7 +35,7 @@ use alacritty_terminal::vi_mode::ViMotion; use crate::clipboard::Clipboard; use crate::config::{Action, Binding, Config, Key, ViAction}; -use crate::event::{ClickState, Event, Mouse}; +use crate::event::{ClickState, Event, Mouse, TYPING_SEARCH_DELAY}; use crate::scheduler::{Scheduler, TimerId}; use crate::url::{Url, Urls}; use crate::window::Window; @@ -93,6 +94,13 @@ pub trait ActionContext<T: EventListener> { fn mouse_mode(&self) -> bool; fn clipboard_mut(&mut self) -> &mut Clipboard; fn scheduler_mut(&mut self) -> &mut Scheduler; + fn start_search(&mut self, direction: Direction); + fn confirm_search(&mut self); + fn cancel_search(&mut self); + fn push_search(&mut self, c: char); + fn pop_search(&mut self); + fn search_direction(&self) -> Direction; + fn search_active(&self) -> bool; } trait Execute<T: EventListener> { @@ -159,6 +167,13 @@ impl<T: EventListener> Execute<T> for Action { }, Action::ClearSelection => ctx.clear_selection(), Action::ToggleViMode => ctx.terminal_mut().toggle_vi_mode(), + Action::ViMotion(motion) => { + if ctx.config().ui_config.mouse.hide_when_typing { + ctx.window_mut().set_mouse_visible(false); + } + + ctx.terminal_mut().vi_motion(motion) + }, Action::ViAction(ViAction::ToggleNormalSelection) => { Self::toggle_selection(ctx, SelectionType::Simple) }, @@ -177,13 +192,44 @@ impl<T: EventListener> Execute<T> for Action { ctx.launch_url(url); } }, - Action::ViMotion(motion) => { - if ctx.config().ui_config.mouse.hide_when_typing { - ctx.window_mut().set_mouse_visible(false); + Action::ViAction(ViAction::SearchNext) => { + let origin = ctx.terminal().visible_to_buffer(ctx.terminal().vi_mode_cursor.point); + let direction = ctx.search_direction(); + + let regex_match = ctx.terminal().search_next(origin, direction, Side::Left, None); + if let Some(regex_match) = regex_match { + ctx.terminal_mut().vi_goto_point(*regex_match.start()); } + }, + Action::ViAction(ViAction::SearchPrevious) => { + let origin = ctx.terminal().visible_to_buffer(ctx.terminal().vi_mode_cursor.point); + let direction = ctx.search_direction().opposite(); - ctx.terminal_mut().vi_motion(motion) + let regex_match = ctx.terminal().search_next(origin, direction, Side::Left, None); + if let Some(regex_match) = regex_match { + ctx.terminal_mut().vi_goto_point(*regex_match.start()); + } + }, + Action::ViAction(ViAction::SearchEndNext) => { + let origin = ctx.terminal().visible_to_buffer(ctx.terminal().vi_mode_cursor.point); + let direction = ctx.search_direction(); + + let regex_match = ctx.terminal().search_next(origin, direction, Side::Right, None); + if let Some(regex_match) = regex_match { + ctx.terminal_mut().vi_goto_point(*regex_match.end()); + } }, + Action::ViAction(ViAction::SearchEndPrevious) => { + let origin = ctx.terminal().visible_to_buffer(ctx.terminal().vi_mode_cursor.point); + let direction = ctx.search_direction().opposite(); + + let regex_match = ctx.terminal().search_next(origin, direction, Side::Right, None); + if let Some(regex_match) = regex_match { + ctx.terminal_mut().vi_goto_point(*regex_match.end()); + } + }, + Action::Search => ctx.start_search(Direction::Right), + Action::SearchReverse => ctx.start_search(Direction::Left), Action::ToggleFullscreen => ctx.window_mut().toggle_fullscreen(), #[cfg(target_os = "macos")] Action::ToggleSimpleFullscreen => ctx.window_mut().toggle_simple_fullscreen(), @@ -199,7 +245,7 @@ impl<T: EventListener> Execute<T> for Action { Action::ScrollPageUp => { // Move vi mode cursor. let term = ctx.terminal_mut(); - let scroll_lines = term.grid().num_lines().0 as isize; + let scroll_lines = term.grid().screen_lines().0 as isize; term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines); ctx.scroll(Scroll::PageUp); @@ -207,7 +253,7 @@ impl<T: EventListener> Execute<T> for Action { Action::ScrollPageDown => { // Move vi mode cursor. let term = ctx.terminal_mut(); - let scroll_lines = -(term.grid().num_lines().0 as isize); + let scroll_lines = -(term.grid().screen_lines().0 as isize); term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines); ctx.scroll(Scroll::PageDown); @@ -215,29 +261,29 @@ impl<T: EventListener> Execute<T> for Action { Action::ScrollHalfPageUp => { // Move vi mode cursor. let term = ctx.terminal_mut(); - let scroll_lines = term.grid().num_lines().0 as isize / 2; + let scroll_lines = term.grid().screen_lines().0 as isize / 2; term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines); - ctx.scroll(Scroll::Lines(scroll_lines)); + ctx.scroll(Scroll::Delta(scroll_lines)); }, Action::ScrollHalfPageDown => { // Move vi mode cursor. let term = ctx.terminal_mut(); - let scroll_lines = -(term.grid().num_lines().0 as isize / 2); + let scroll_lines = -(term.grid().screen_lines().0 as isize / 2); term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines); - ctx.scroll(Scroll::Lines(scroll_lines)); + ctx.scroll(Scroll::Delta(scroll_lines)); }, Action::ScrollLineUp => { // Move vi mode cursor. let term = ctx.terminal(); if term.grid().display_offset() != term.grid().history_size() - && term.vi_mode_cursor.point.line + 1 != term.grid().num_lines() + && term.vi_mode_cursor.point.line + 1 != term.grid().screen_lines() { ctx.terminal_mut().vi_mode_cursor.point.line += 1; } - ctx.scroll(Scroll::Lines(1)); + ctx.scroll(Scroll::Delta(1)); }, Action::ScrollLineDown => { // Move vi mode cursor. @@ -247,7 +293,7 @@ impl<T: EventListener> Execute<T> for Action { ctx.terminal_mut().vi_mode_cursor.point.line -= 1; } - ctx.scroll(Scroll::Lines(-1)); + ctx.scroll(Scroll::Delta(-1)); }, Action::ScrollToTop => { ctx.scroll(Scroll::Top); @@ -261,7 +307,7 @@ impl<T: EventListener> Execute<T> for Action { // Move vi mode cursor. let term = ctx.terminal_mut(); - term.vi_mode_cursor.point.line = term.grid().num_lines() - 1; + term.vi_mode_cursor.point.line = term.grid().screen_lines() - 1; // Move to beginning twice, to always jump across linewraps. term.vi_motion(ViMotion::FirstOccupied); @@ -360,7 +406,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> { self.update_url_state(&mouse_state); self.ctx.window_mut().set_mouse_cursor(mouse_state.into()); - let last_term_line = self.ctx.terminal().grid().num_lines() - 1; + let last_term_line = self.ctx.terminal().grid().screen_lines() - 1; if (lmb_pressed || self.ctx.mouse().right_button_state == ElementState::Pressed) && (self.ctx.modifiers().shift() || !self.ctx.mouse_mode()) { @@ -520,7 +566,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> { // Load mouse point, treating message bar and padding as the closest cell. let mouse = self.ctx.mouse(); let mut point = self.ctx.size_info().pixels_to_coords(mouse.x, mouse.y); - point.line = min(point.line, self.ctx.terminal().grid().num_lines() - 1); + point.line = min(point.line, self.ctx.terminal().grid().screen_lines() - 1); match button { MouseButton::Left => self.on_left_click(point), @@ -690,7 +736,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> { let term = self.ctx.terminal(); let absolute = term.visible_to_buffer(term.vi_mode_cursor.point); - self.ctx.scroll(Scroll::Lines(lines as isize)); + self.ctx.scroll(Scroll::Delta(lines as isize)); // Try to restore vi mode cursor position, to keep it above its previous content. let term = self.ctx.terminal_mut(); @@ -733,7 +779,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> { // Reset cursor when message bar height changed or all messages are gone. let size = self.ctx.size_info(); - let current_lines = (size.lines() - self.ctx.terminal().grid().num_lines()).0; + let current_lines = (size.lines() - self.ctx.terminal().grid().screen_lines()).0; let new_lines = self.ctx.message().map(|m| m.text(&size).len()).unwrap_or(0); let new_icon = match current_lines.cmp(&new_lines) { @@ -749,7 +795,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> { }; self.ctx.window_mut().set_mouse_cursor(new_icon); - } else { + } else if !self.ctx.search_active() { match state { ElementState::Pressed => { self.process_mouse_bindings(button); @@ -763,6 +809,28 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> { /// Process key input. pub fn key_input(&mut self, input: KeyboardInput) { match input.state { + ElementState::Pressed if self.ctx.search_active() => { + match input.virtual_keycode { + Some(VirtualKeyCode::Back) => { + self.ctx.pop_search(); + *self.ctx.suppress_chars() = true; + }, + Some(VirtualKeyCode::Return) => { + self.ctx.confirm_search(); + *self.ctx.suppress_chars() = true; + }, + Some(VirtualKeyCode::Escape) => { + self.ctx.cancel_search(); + *self.ctx.suppress_chars() = true; + }, + _ => (), + } + + // Reset search delay when the user is still typing. + if let Some(timer) = self.ctx.scheduler_mut().get_mut(TimerId::DelayedSearch) { + timer.deadline = Instant::now() + TYPING_SEARCH_DELAY; + } + }, ElementState::Pressed => { *self.ctx.received_count() = 0; self.process_key_bindings(input); @@ -783,7 +851,19 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> { /// Process a received character. pub fn received_char(&mut self, c: char) { - if *self.ctx.suppress_chars() || self.ctx.terminal().mode().contains(TermMode::VI) { + let suppress_chars = *self.ctx.suppress_chars(); + let search_active = self.ctx.search_active(); + if suppress_chars || self.ctx.terminal().mode().contains(TermMode::VI) || search_active { + if search_active { + // Skip control characters. + let c_decimal = c as isize; + let is_printable = (c_decimal >= 0x20 && c_decimal < 0x7f) || c_decimal >= 0xa0; + + if !suppress_chars && is_printable { + self.ctx.push_search(c); + } + } + return; } @@ -877,15 +957,19 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> { /// Check if the cursor is hovering above the message bar. fn message_at_cursor(&mut self) -> bool { - self.ctx.mouse().line >= self.ctx.terminal().grid().num_lines() + self.ctx.mouse().line >= self.ctx.terminal().grid().screen_lines() } /// Whether the point is over the message bar's close button. fn message_close_at_cursor(&self) -> bool { let mouse = self.ctx.mouse(); + + // Since search is above the message bar, the button is offset by search's height. + let search_height = if self.ctx.search_active() { 1 } else { 0 }; + mouse.inside_grid && mouse.column + message_bar::CLOSE_BUTTON_TEXT.len() >= self.ctx.size_info().cols() - && mouse.line == self.ctx.terminal().grid().num_lines() + && mouse.line == self.ctx.terminal().grid().screen_lines() + search_height } /// Copy text selection. @@ -967,7 +1051,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> { // Scale number of lines scrolled based on distance to boundary. let delta = delta as isize / step as isize; - let event = Event::Scroll(Scroll::Lines(delta)); + let event = Event::Scroll(Scroll::Delta(delta)); // Schedule event. match scheduler.get_mut(TimerId::SelectionScrolling) { @@ -1036,6 +1120,24 @@ mod tests { fn reset_font_size(&mut self) {} + fn start_search(&mut self, _direction: Direction) {} + + fn confirm_search(&mut self) {} + + fn cancel_search(&mut self) {} + + fn push_search(&mut self, _c: char) {} + + fn pop_search(&mut self) {} + + fn search_direction(&self) -> Direction { + Direction::Right + } + + fn search_active(&self) -> bool { + false + } + fn terminal(&self) -> &Term<T> { &self.terminal } diff --git a/alacritty/src/renderer/mod.rs b/alacritty/src/renderer/mod.rs index b2940a93..7dc037a1 100644 --- a/alacritty/src/renderer/mod.rs +++ b/alacritty/src/renderer/mod.rs @@ -298,7 +298,7 @@ impl GlyphCache { pub fn update_font_size<L: LoadGlyph>( &mut self, - font: config::Font, + font: &config::Font, dpr: f64, loader: &mut L, ) -> Result<(), font::Error> { @@ -307,7 +307,7 @@ impl GlyphCache { // Recompute font keys. let (regular, bold, italic, bold_italic) = - Self::compute_font_keys(&font, &mut self.rasterizer)?; + Self::compute_font_keys(font, &mut self.rasterizer)?; self.rasterizer.get_glyph(GlyphKey { font_key: regular, c: 'm', size: font.size })?; let metrics = self.rasterizer.metrics(regular, font.size)?; @@ -968,29 +968,29 @@ impl<'a, C> RenderApi<'a, C> { /// errors. pub fn render_string( &mut self, - string: &str, - line: Line, glyph_cache: &mut GlyphCache, - color: Option<Rgb>, + line: Line, + string: &str, + fg: Rgb, + bg: Option<Rgb>, ) { - let bg_alpha = color.map(|_| 1.0).unwrap_or(0.0); - let col = Column(0); + let bg_alpha = bg.map(|_| 1.0).unwrap_or(0.0); let cells = string .chars() .enumerate() .map(|(i, c)| RenderableCell { line, - column: col + i, + column: Column(i), inner: RenderableCellContent::Chars({ let mut chars = [' '; cell::MAX_ZEROWIDTH_CHARS + 1]; chars[0] = c; chars }), - bg: color.unwrap_or(Rgb { r: 0, g: 0, b: 0 }), - fg: Rgb { r: 0, g: 0, b: 0 }, flags: Flags::empty(), bg_alpha, + fg, + bg: bg.unwrap_or(Rgb { r: 0, g: 0, b: 0 }), }) .collect::<Vec<_>>(); diff --git a/alacritty/src/scheduler.rs b/alacritty/src/scheduler.rs index 673029ae..db6180ca 100644 --- a/alacritty/src/scheduler.rs +++ b/alacritty/src/scheduler.rs @@ -9,6 +9,22 @@ use crate::event::Event as AlacrittyEvent; type Event = GlutinEvent<'static, AlacrittyEvent>; +/// ID uniquely identifying a timer. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum TimerId { + SelectionScrolling, + DelayedSearch, +} + +/// Event scheduled to be emitted at a specific time. +pub struct Timer { + pub deadline: Instant, + pub event: Event, + + interval: Option<Duration>, + id: TimerId, +} + /// Scheduler tracking all pending timers. pub struct Scheduler { timers: VecDeque<Timer>, @@ -74,23 +90,13 @@ impl Scheduler { self.timers.remove(index).map(|timer| timer.event) } + /// Check if a timer is already scheduled. + pub fn scheduled(&mut self, id: TimerId) -> bool { + self.timers.iter().any(|timer| timer.id == id) + } + /// Access a staged event by ID. pub fn get_mut(&mut self, id: TimerId) -> Option<&mut Timer> { self.timers.iter_mut().find(|timer| timer.id == id) } } - -/// ID uniquely identifying a timer. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum TimerId { - SelectionScrolling, -} - -/// Event scheduled to be emitted at a specific time. -pub struct Timer { - pub deadline: Instant, - pub event: Event, - - interval: Option<Duration>, - id: TimerId, -} diff --git a/alacritty/src/url.rs b/alacritty/src/url.rs index f7c7105c..08a85f1b 100644 --- a/alacritty/src/url.rs +++ b/alacritty/src/url.rs @@ -90,7 +90,7 @@ impl Urls { self.last_point = Some(end); // Extend current state if a wide char spacer is encountered. - if cell.flags.contains(Flags::WIDE_CHAR_SPACER) { + if cell.flags.intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER) { if let UrlLocation::Url(_, mut end_offset) = self.state { if end_offset != 0 { end_offset += 1; diff --git a/alacritty/src/window.rs b/alacritty/src/window.rs index 4275f859..450329d4 100644 --- a/alacritty/src/window.rs +++ b/alacritty/src/window.rs @@ -35,7 +35,9 @@ use winapi::shared::minwindef::WORD; use alacritty_terminal::config::{Decorations, StartupMode, WindowConfig}; #[cfg(not(windows))] -use alacritty_terminal::term::{SizeInfo, Term}; +use alacritty_terminal::index::Point; +#[cfg(not(windows))] +use alacritty_terminal::term::SizeInfo; use crate::config::Config; use crate::gl; @@ -398,8 +400,7 @@ impl Window { /// Adjust the IME editor position according to the new location of the cursor. #[cfg(not(windows))] - pub fn update_ime_position<T>(&mut self, terminal: &Term<T>, size_info: &SizeInfo) { - let point = terminal.grid().cursor.point; + pub fn update_ime_position(&mut self, point: Point, size_info: &SizeInfo) { let SizeInfo { cell_width, cell_height, padding_x, padding_y, .. } = size_info; let nspot_x = f64::from(padding_x + point.col.0 as f32 * cell_width); diff --git a/alacritty_terminal/Cargo.toml b/alacritty_terminal/Cargo.toml index 65a12c6f..9345a76f 100644 --- a/alacritty_terminal/Cargo.toml +++ b/alacritty_terminal/Cargo.toml @@ -22,6 +22,7 @@ log = "0.4" unicode-width = "0.1" base64 = "0.12.0" terminfo = "0.7.1" +regex-automata = "0.1.9" [target.'cfg(unix)'.dependencies] nix = "0.17.0" diff --git a/alacritty_terminal/src/ansi.rs b/alacritty_terminal/src/ansi.rs index 8240ff00..5f24dcff 100644 --- a/alacritty_terminal/src/ansi.rs +++ b/alacritty_terminal/src/ansi.rs @@ -89,13 +89,13 @@ struct ProcessorState { /// /// Processor creates a Performer when running advance and passes the Performer /// to `vte::Parser`. -struct Performer<'a, H: Handler + TermInfo, W: io::Write> { +struct Performer<'a, H: Handler, W: io::Write> { state: &'a mut ProcessorState, handler: &'a mut H, writer: &'a mut W, } -impl<'a, H: Handler + TermInfo + 'a, W: io::Write> Performer<'a, H, W> { +impl<'a, H: Handler + 'a, W: io::Write> Performer<'a, H, W> { /// Create a performer. #[inline] pub fn new<'b>( @@ -121,7 +121,7 @@ impl Processor { #[inline] pub fn advance<H, W>(&mut self, handler: &mut H, byte: u8, writer: &mut W) where - H: Handler + TermInfo, + H: Handler, W: io::Write, { let mut performer = Performer::new(&mut self.state, handler, writer); @@ -129,12 +129,6 @@ impl Processor { } } -/// Trait that provides properties of terminal. -pub trait TermInfo { - fn lines(&self) -> Line; - fn cols(&self) -> Column; -} - /// Type that handles actions from the parser. /// /// XXX Should probably not provide default impls for everything, but it makes @@ -278,7 +272,7 @@ pub trait Handler { fn unset_mode(&mut self, _: Mode) {} /// DECSTBM - Set the terminal scrolling region. - fn set_scrolling_region(&mut self, _top: usize, _bottom: usize) {} + fn set_scrolling_region(&mut self, _top: usize, _bottom: Option<usize>) {} /// DECKPAM - Set keypad to applications mode (ESCape instead of digits). fn set_keypad_application_mode(&mut self) {} @@ -731,7 +725,7 @@ impl StandardCharset { impl<'a, H, W> vte::Perform for Performer<'a, H, W> where - H: Handler + TermInfo + 'a, + H: Handler + 'a, W: io::Write + 'a, { #[inline] @@ -945,9 +939,7 @@ where macro_rules! arg_or_default { (idx: $idx:expr, default: $default:expr) => { - args.get($idx) - .and_then(|v| if *v == 0 { None } else { Some(*v) }) - .unwrap_or($default) + args.get($idx).copied().filter(|&v| v != 0).unwrap_or($default) }; } @@ -1099,7 +1091,7 @@ where }, ('r', None) => { let top = arg_or_default!(idx: 0, default: 1) as usize; - let bottom = arg_or_default!(idx: 1, default: handler.lines().0 as _) as usize; + let bottom = args.get(1).map(|&b| b as usize).filter(|&b| b != 0); handler.set_scrolling_region(top, bottom); }, @@ -1391,9 +1383,7 @@ pub mod C0 { mod tests { use super::{ parse_number, xparse_color, Attr, CharsetIndex, Color, Handler, Processor, StandardCharset, - TermInfo, }; - use crate::index::{Column, Line}; use crate::term::color::Rgb; use std::io; @@ -1427,16 +1417,6 @@ mod tests { } } - impl TermInfo for MockHandler { - fn lines(&self) -> Line { - Line(200) - } - - fn cols(&self) -> Column { - Column(90) - } - } - impl Default for MockHandler { fn default() -> MockHandler { MockHandler { diff --git a/alacritty_terminal/src/config/colors.rs b/alacritty_terminal/src/config/colors.rs index ccea9536..13a30bef 100644 --- a/alacritty_terminal/src/config/colors.rs +++ b/alacritty_terminal/src/config/colors.rs @@ -1,8 +1,9 @@ use log::error; use serde::{Deserialize, Deserializer}; +use serde_yaml::Value; use crate::config::{failure_default, LOG_TARGET_CONFIG}; -use crate::term::color::Rgb; +use crate::term::color::{CellRgb, Rgb}; #[serde(default)] #[derive(Deserialize, Clone, Debug, Default, PartialEq, Eq)] @@ -23,6 +24,8 @@ pub struct Colors { pub dim: Option<AnsiColors>, #[serde(deserialize_with = "failure_default")] pub indexed_colors: Vec<IndexedColor>, + #[serde(deserialize_with = "failure_default")] + pub search: SearchColors, } impl Colors { @@ -33,6 +36,32 @@ impl Colors { pub fn bright(&self) -> &AnsiColors { &self.bright.0 } + + pub fn search_bar_foreground(&self) -> Rgb { + self.search.bar.foreground.unwrap_or(self.primary.background) + } + + pub fn search_bar_background(&self) -> Rgb { + self.search.bar.background.unwrap_or(self.primary.foreground) + } +} + +#[derive(Deserialize, Copy, Clone, Debug, PartialEq, Eq)] +struct DefaultForegroundCellRgb(CellRgb); + +impl Default for DefaultForegroundCellRgb { + fn default() -> Self { + Self(CellRgb::CellForeground) + } +} + +#[derive(Deserialize, Copy, Clone, Debug, PartialEq, Eq)] +struct DefaultBackgroundCellRgb(CellRgb); + +impl Default for DefaultBackgroundCellRgb { + fn default() -> Self { + Self(CellRgb::CellBackground) + } } #[serde(default)] @@ -44,11 +73,11 @@ pub struct IndexedColor { pub color: Rgb, } -fn deserialize_color_index<'a, D>(deserializer: D) -> ::std::result::Result<u8, D::Error> +fn deserialize_color_index<'a, D>(deserializer: D) -> Result<u8, D::Error> where D: Deserializer<'a>, { - let value = serde_yaml::Value::deserialize(deserializer)?; + let value = Value::deserialize(deserializer)?; match u8::deserialize(value) { Ok(index) => { if index < 16 { @@ -78,26 +107,91 @@ where #[derive(Deserialize, Debug, Copy, Clone, Default, PartialEq, Eq)] pub struct CursorColors { #[serde(deserialize_with = "failure_default")] - pub text: Option<Rgb>, + text: DefaultBackgroundCellRgb, #[serde(deserialize_with = "failure_default")] - pub cursor: Option<Rgb>, + cursor: DefaultForegroundCellRgb, +} + +impl CursorColors { + pub fn text(self) -> CellRgb { + self.text.0 + } + + pub fn cursor(self) -> CellRgb { + self.cursor.0 + } } #[serde(default)] #[derive(Deserialize, Debug, Copy, Clone, Default, PartialEq, Eq)] pub struct SelectionColors { #[serde(deserialize_with = "failure_default")] - pub text: Option<Rgb>, + text: DefaultBackgroundCellRgb, + #[serde(deserialize_with = "failure_default")] + background: DefaultForegroundCellRgb, +} + +impl SelectionColors { + pub fn text(self) -> CellRgb { + self.text.0 + } + + pub fn background(self) -> CellRgb { + self.background.0 + } +} + +#[serde(default)] +#[derive(Deserialize, Debug, Copy, Clone, Default, PartialEq, Eq)] +pub struct SearchColors { + #[serde(deserialize_with = "failure_default")] + pub matches: MatchColors, #[serde(deserialize_with = "failure_default")] - pub background: Option<Rgb>, + bar: BarColors, +} + +#[serde(default)] +#[derive(Deserialize, Debug, Copy, Clone, PartialEq, Eq)] +pub struct MatchColors { + #[serde(deserialize_with = "failure_default")] + pub foreground: CellRgb, + #[serde(deserialize_with = "deserialize_match_background")] + pub background: CellRgb, +} + +impl Default for MatchColors { + fn default() -> Self { + Self { foreground: CellRgb::default(), background: default_match_background() } + } +} + +fn deserialize_match_background<'a, D>(deserializer: D) -> Result<CellRgb, D::Error> +where + D: Deserializer<'a>, +{ + let value = Value::deserialize(deserializer)?; + Ok(CellRgb::deserialize(value).unwrap_or_else(|_| default_match_background())) +} + +fn default_match_background() -> CellRgb { + CellRgb::Rgb(Rgb { r: 0xff, g: 0xff, b: 0xff }) +} + +#[serde(default)] +#[derive(Deserialize, Debug, Copy, Clone, Default, PartialEq, Eq)] +pub struct BarColors { + #[serde(deserialize_with = "failure_default")] + foreground: Option<Rgb>, + #[serde(deserialize_with = "failure_default")] + background: Option<Rgb>, } #[serde(default)] #[derive(Deserialize, Clone, Debug, PartialEq, Eq)] pub struct PrimaryColors { - #[serde(default = "default_background", deserialize_with = "failure_default")] + #[serde(deserialize_with = "failure_default")] pub background: Rgb, - #[serde(default = "default_foreground", deserialize_with = "failure_default")] + #[serde(deserialize_with = "failure_default")] pub foreground: Rgb, #[serde(deserialize_with = "failure_default")] pub bright_foreground: Option<Rgb>, @@ -108,22 +202,14 @@ pub struct PrimaryColors { impl Default for PrimaryColors { fn default() -> Self { PrimaryColors { - background: default_background(), - foreground: default_foreground(), + background: Rgb { r: 0x1d, g: 0x1f, b: 0x21 }, + foreground: Rgb { r: 0xc5, g: 0xc8, b: 0xc6 }, bright_foreground: Default::default(), dim_foreground: Default::default(), } } } -fn default_background() -> Rgb { - Rgb { r: 0x1d, g: 0x1f, b: 0x21 } -} - -fn default_foreground() -> Rgb { - Rgb { r: 0xc5, g: 0xc8, b: 0xc6 } -} - /// The 8-colors sections of config. #[derive(Deserialize, Clone, Debug, PartialEq, Eq)] pub struct AnsiColors { diff --git a/alacritty_terminal/src/config/mod.rs b/alacritty_terminal/src/config/mod.rs index c3936c0c..e3d72fda 100644 --- a/alacritty_terminal/src/config/mod.rs +++ b/alacritty_terminal/src/config/mod.rs @@ -13,7 +13,7 @@ mod scrolling; mod visual_bell; mod window; -use crate::ansi::{CursorStyle, NamedColor}; +use crate::ansi::CursorStyle; pub use crate::config::colors::Colors; pub use crate::config::debug::Debug; @@ -21,7 +21,6 @@ pub use crate::config::font::{Font, FontDescription}; pub use crate::config::scrolling::Scrolling; pub use crate::config::visual_bell::{VisualBellAnimation, VisualBellConfig}; pub use crate::config::window::{Decorations, Dimensions, StartupMode, WindowConfig, DEFAULT_NAME}; -use crate::term::color::Rgb; pub const LOG_TARGET_CONFIG: &str = "alacritty_config"; const MAX_SCROLLBACK_LINES: u32 = 100_000; @@ -156,30 +155,6 @@ impl<T> Config<T> { self.dynamic_title.0 } - /// Cursor foreground color. - #[inline] - pub fn cursor_text_color(&self) -> Option<Rgb> { - self.colors.cursor.text - } - - /// Cursor background color. - #[inline] - pub fn cursor_cursor_color(&self) -> Option<NamedColor> { - self.colors.cursor.cursor.map(|_| NamedColor::Cursor) - } - - /// Vi mode cursor foreground color. - #[inline] - pub fn vi_mode_cursor_text_color(&self) -> Option<Rgb> { - self.colors.vi_mode_cursor.text - } - - /// Vi mode cursor background color. - #[inline] - pub fn vi_mode_cursor_cursor_color(&self) -> Option<Rgb> { - self.colors.vi_mode_cursor.cursor - } - #[inline] pub fn set_dynamic_title(&mut self, dynamic_title: bool) { self.dynamic_title.0 = dynamic_title; diff --git a/alacritty_terminal/src/grid/mod.rs b/alacritty_terminal/src/grid/mod.rs index 5ad7e8d6..c1f980e8 100644 --- a/alacritty_terminal/src/grid/mod.rs +++ b/alacritty_terminal/src/grid/mod.rs @@ -160,7 +160,7 @@ pub struct Grid<T> { #[derive(Debug, Copy, Clone)] pub enum Scroll { - Lines(isize), + Delta(isize), PageUp, PageDown, Top, @@ -180,23 +180,6 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> { } } - /// Clamp a buffer point to the visible region. - pub fn clamp_buffer_to_visible(&self, point: Point<usize>) -> Point { - if point.line < self.display_offset { - Point::new(self.lines - 1, self.cols - 1) - } else if point.line >= self.display_offset + self.lines.0 { - Point::new(Line(0), Column(0)) - } else { - // Since edge-cases are handled, conversion is identical as visible to buffer. - self.visible_to_buffer(point.into()).into() - } - } - - /// Convert viewport relative point to global buffer indexing. - pub fn visible_to_buffer(&self, point: Point) -> Point<usize> { - Point { line: self.lines.0 + self.display_offset - point.line.0 - 1, col: point.col } - } - /// Update the size of the scrollback history. pub fn update_history(&mut self, history_size: usize) { let current_history_size = self.history_size(); @@ -208,22 +191,16 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> { } pub fn scroll_display(&mut self, scroll: Scroll) { - match scroll { - Scroll::Lines(count) => { - self.display_offset = min( - max((self.display_offset as isize) + count, 0isize) as usize, - self.history_size(), - ); - }, - Scroll::PageUp => { - self.display_offset = min(self.display_offset + self.lines.0, self.history_size()); - }, - Scroll::PageDown => { - self.display_offset -= min(self.display_offset, self.lines.0); - }, - Scroll::Top => self.display_offset = self.history_size(), - Scroll::Bottom => self.display_offset = 0, - } + self.display_offset = match scroll { + Scroll::Delta(count) => min( + max((self.display_offset as isize) + count, 0isize) as usize, + self.history_size(), + ), + Scroll::PageUp => min(self.display_offset + self.lines.0, self.history_size()), + Scroll::PageDown => self.display_offset.saturating_sub(self.lines.0), + Scroll::Top => self.history_size(), + Scroll::Bottom => 0, + }; } fn increase_scroll_limit(&mut self, count: usize, template: T) { @@ -279,7 +256,7 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> { /// /// This is the performance-sensitive part of scrolling. pub fn scroll_up(&mut self, region: &Range<Line>, positions: Line, template: T) { - let num_lines = self.num_lines().0; + let num_lines = self.screen_lines().0; if region.start == Line(0) { // Update display offset when not pinned to active area. @@ -324,7 +301,7 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> { pub fn clear_viewport(&mut self, template: T) { // Determine how many lines to scroll up by. - let end = Point { line: 0, col: self.num_cols() }; + let end = Point { line: 0, col: self.cols() }; let mut iter = self.iter_from(end); while let Some(cell) = iter.prev() { if !cell.is_empty() || iter.cur.line >= *self.lines { @@ -333,7 +310,7 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> { } debug_assert!(iter.cur.line <= *self.lines); let positions = self.lines - iter.cur.line; - let region = Line(0)..self.num_lines(); + let region = Line(0)..self.screen_lines(); // Reset display offset. self.display_offset = 0; @@ -364,19 +341,27 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> { #[allow(clippy::len_without_is_empty)] impl<T> Grid<T> { - #[inline] - pub fn num_lines(&self) -> Line { - self.lines + /// Clamp a buffer point to the visible region. + pub fn clamp_buffer_to_visible(&self, point: Point<usize>) -> Point { + if point.line < self.display_offset { + Point::new(self.lines - 1, self.cols - 1) + } else if point.line >= self.display_offset + self.lines.0 { + Point::new(Line(0), Column(0)) + } else { + // Since edgecases are handled, conversion is identical as visible to buffer. + self.visible_to_buffer(point.into()).into() + } } + /// Convert viewport relative point to global buffer indexing. #[inline] - pub fn display_iter(&self) -> DisplayIter<'_, T> { - DisplayIter::new(self) + pub fn visible_to_buffer(&self, point: Point) -> Point<usize> { + Point { line: self.lines.0 + self.display_offset - point.line.0 - 1, col: point.col } } #[inline] - pub fn num_cols(&self) -> Column { - self.cols + pub fn display_iter(&self) -> DisplayIter<'_, T> { + DisplayIter::new(self) } #[inline] @@ -385,17 +370,6 @@ impl<T> Grid<T> { self.raw.shrink_lines(self.history_size()); } - /// Total number of lines in the buffer, this includes scrollback + visible lines. - #[inline] - pub fn len(&self) -> usize { - self.raw.len() - } - - #[inline] - pub fn history_size(&self) -> usize { - self.raw.len() - *self.lines - } - /// This is used only for initializing after loading ref-tests. #[inline] pub fn initialize_all(&mut self, template: T) @@ -432,6 +406,56 @@ impl<T> Grid<T> { } } +/// Grid dimensions. +pub trait Dimensions { + /// Total number of lines in the buffer, this includes scrollback and visible lines. + fn total_lines(&self) -> usize; + + /// Height of the viewport in lines. + fn screen_lines(&self) -> Line; + + /// Width of the terminal in columns. + fn cols(&self) -> Column; + + /// Number of invisible lines part of the scrollback history. + #[inline] + fn history_size(&self) -> usize { + self.total_lines() - self.screen_lines().0 + } +} + +impl<G> Dimensions for Grid<G> { + #[inline] + fn total_lines(&self) -> usize { + self.raw.len() + } + + #[inline] + fn screen_lines(&self) -> Line { + self.lines + } + + #[inline] + fn cols(&self) -> Column { + self.cols + } +} + +#[cfg(test)] +impl Dimensions for (Line, Column) { + fn total_lines(&self) -> usize { + *self.0 + } + + fn screen_lines(&self) -> Line { + self.0 + } + + fn cols(&self) -> Column { + self.1 + } +} + pub struct GridIterator<'a, T> { /// Immutable grid reference. grid: &'a Grid<T>, @@ -446,7 +470,7 @@ impl<'a, T> GridIterator<'a, T> { } pub fn cell(&self) -> &'a T { - &self.grid[self.cur.line][self.cur.col] + &self.grid[self.cur] } } @@ -454,38 +478,35 @@ impl<'a, T> Iterator for GridIterator<'a, T> { type Item = &'a T; fn next(&mut self) -> Option<Self::Item> { - let last_col = self.grid.num_cols() - Column(1); + let last_col = self.grid.cols() - 1; + match self.cur { - Point { line, col } if line == 0 && col == last_col => None, + Point { line, col } if line == 0 && col == last_col => return None, Point { col, .. } if (col == last_col) => { self.cur.line -= 1; self.cur.col = Column(0); - Some(&self.grid[self.cur.line][self.cur.col]) - }, - _ => { - self.cur.col += Column(1); - Some(&self.grid[self.cur.line][self.cur.col]) }, + _ => self.cur.col += Column(1), } + + Some(&self.grid[self.cur]) } } impl<'a, T> BidirectionalIterator for GridIterator<'a, T> { fn prev(&mut self) -> Option<Self::Item> { - let num_cols = self.grid.num_cols(); + let last_col = self.grid.cols() - 1; match self.cur { - Point { line, col: Column(0) } if line == self.grid.len() - 1 => None, + Point { line, col: Column(0) } if line == self.grid.total_lines() - 1 => return None, Point { col: Column(0), .. } => { self.cur.line += 1; - self.cur.col = num_cols - Column(1); - Some(&self.grid[self.cur.line][self.cur.col]) - }, - _ => { - self.cur.col -= Column(1); - Some(&self.grid[self.cur.line][self.cur.col]) + self.cur.col = last_col; }, + _ => self.cur.col -= Column(1), } + + Some(&self.grid[self.cur]) } } @@ -539,6 +560,22 @@ impl<'point, T> IndexMut<&'point Point> for Grid<T> { } } +impl<T> Index<Point<usize>> for Grid<T> { + type Output = T; + + #[inline] + fn index(&self, point: Point<usize>) -> &T { + &self[point.line][point.col] + } +} + +impl<T> IndexMut<Point<usize>> for Grid<T> { + #[inline] + fn index_mut(&mut self, point: Point<usize>) -> &mut T { + &mut self[point.line][point.col] + } +} + /// A subset of lines in the grid. /// /// May be constructed using Grid::region(..). @@ -578,15 +615,15 @@ pub trait IndexRegion<I, T> { impl<T> IndexRegion<Range<Line>, T> for Grid<T> { fn region(&self, index: Range<Line>) -> Region<'_, T> { - assert!(index.start < self.num_lines()); - assert!(index.end <= self.num_lines()); + assert!(index.start < self.screen_lines()); + assert!(index.end <= self.screen_lines()); assert!(index.start <= index.end); Region { start: index.start, end: index.end, raw: &self.raw } } fn region_mut(&mut self, index: Range<Line>) -> RegionMut<'_, T> { - assert!(index.start < self.num_lines()); - assert!(index.end <= self.num_lines()); + assert!(index.start < self.screen_lines()); + assert!(index.end <= self.screen_lines()); assert!(index.start <= index.end); RegionMut { start: index.start, end: index.end, raw: &mut self.raw } } @@ -594,35 +631,35 @@ impl<T> IndexRegion<Range<Line>, T> for Grid<T> { impl<T> IndexRegion<RangeTo<Line>, T> for Grid<T> { fn region(&self, index: RangeTo<Line>) -> Region<'_, T> { - assert!(index.end <= self.num_lines()); + assert!(index.end <= self.screen_lines()); Region { start: Line(0), end: index.end, raw: &self.raw } } fn region_mut(&mut self, index: RangeTo<Line>) -> RegionMut<'_, T> { - assert!(index.end <= self.num_lines()); + assert!(index.end <= self.screen_lines()); RegionMut { start: Line(0), end: index.end, raw: &mut self.raw } } } impl<T> IndexRegion<RangeFrom<Line>, T> for Grid<T> { fn region(&self, index: RangeFrom<Line>) -> Region<'_, T> { - assert!(index.start < self.num_lines()); - Region { start: index.start, end: self.num_lines(), raw: &self.raw } + assert!(index.start < self.screen_lines()); + Region { start: index.start, end: self.screen_lines(), raw: &self.raw } } fn region_mut(&mut self, index: RangeFrom<Line>) -> RegionMut<'_, T> { - assert!(index.start < self.num_lines()); - RegionMut { start: index.start, end: self.num_lines(), raw: &mut self.raw } + assert!(index.start < self.screen_lines()); + RegionMut { start: index.start, end: self.screen_lines(), raw: &mut self.raw } } } impl<T> IndexRegion<RangeFull, T> for Grid<T> { fn region(&self, _: RangeFull) -> Region<'_, T> { - Region { start: Line(0), end: self.num_lines(), raw: &self.raw } + Region { start: Line(0), end: self.screen_lines(), raw: &self.raw } } fn region_mut(&mut self, _: RangeFull) -> RegionMut<'_, T> { - RegionMut { start: Line(0), end: self.num_lines(), raw: &mut self.raw } + RegionMut { start: Line(0), end: self.screen_lines(), raw: &mut self.raw } } } @@ -695,7 +732,7 @@ pub struct DisplayIter<'a, T> { impl<'a, T: 'a> DisplayIter<'a, T> { pub fn new(grid: &'a Grid<T>) -> DisplayIter<'a, T> { - let offset = grid.display_offset + *grid.num_lines() - 1; + let offset = grid.display_offset + *grid.screen_lines() - 1; let limit = grid.display_offset; let col = Column(0); let line = Line(0); @@ -722,7 +759,7 @@ impl<'a, T: Copy + 'a> Iterator for DisplayIter<'a, T> { #[inline] fn next(&mut self) -> Option<Self::Item> { // Return None if we've reached the end. - if self.offset == self.limit && self.grid.num_cols() == self.col { + if self.offset == self.limit && self.grid.cols() == self.col { return None; } @@ -735,7 +772,7 @@ impl<'a, T: Copy + 'a> Iterator for DisplayIter<'a, T> { // Update line/col to point to next item. self.col += 1; - if self.col == self.grid.num_cols() && self.offset != self.limit { + if self.col == self.grid.cols() && self.offset != self.limit { self.offset -= 1; self.col = Column(0); diff --git a/alacritty_terminal/src/grid/resize.rs b/alacritty_terminal/src/grid/resize.rs index a0493fc0..079fcf19 100644 --- a/alacritty_terminal/src/grid/resize.rs +++ b/alacritty_terminal/src/grid/resize.rs @@ -6,7 +6,7 @@ use crate::index::{Column, Line}; use crate::term::cell::Flags; use crate::grid::row::Row; -use crate::grid::{Grid, GridCell}; +use crate::grid::{Dimensions, Grid, GridCell}; impl<T: GridCell + Default + PartialEq + Copy> Grid<T> { /// Resize the grid's width and/or height. @@ -18,8 +18,8 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> { } match self.cols.cmp(&cols) { - Ordering::Less => self.grow_cols(cols, reflow), - Ordering::Greater => self.shrink_cols(cols, reflow), + Ordering::Less => self.grow_cols(reflow, cols), + Ordering::Greater => self.shrink_cols(reflow, cols), Ordering::Equal => (), } } @@ -79,7 +79,7 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> { } /// Grow number of columns in each row, reflowing if necessary. - fn grow_cols(&mut self, cols: Column, reflow: bool) { + fn grow_cols(&mut self, reflow: bool, cols: Column) { // Check if a row needs to be wrapped. let should_reflow = |row: &Row<T>| -> bool { let len = Column(row.len()); @@ -116,9 +116,8 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> { // Remove leading spacers when reflowing wide char to the previous line. let mut last_len = last_row.len(); - if last_len >= 2 - && !last_row[Column(last_len - 2)].flags().contains(Flags::WIDE_CHAR) - && last_row[Column(last_len - 1)].flags().contains(Flags::WIDE_CHAR_SPACER) + if last_len >= 1 + && last_row[Column(last_len - 1)].flags().contains(Flags::LEADING_WIDE_CHAR_SPACER) { last_row.shrink(Column(last_len - 1)); last_len -= 1; @@ -135,7 +134,7 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> { let mut cells = row.front_split_off(len - 1); let mut spacer = T::default(); - spacer.flags_mut().insert(Flags::WIDE_CHAR_SPACER); + spacer.flags_mut().insert(Flags::LEADING_WIDE_CHAR_SPACER); cells.push(spacer); cells @@ -143,7 +142,7 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> { row.front_split_off(len) }; - // Reflow cells to previous row. + // Add removed cells to previous row and reflow content. last_row.append(&mut cells); let cursor_buffer_line = (self.lines - self.cursor.point.line - 1).0; @@ -219,7 +218,7 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> { } /// Shrink number of columns in each row, reflowing if necessary. - fn shrink_cols(&mut self, cols: Column, reflow: bool) { + fn shrink_cols(&mut self, reflow: bool, cols: Column) { self.cols = cols; // Remove the linewrap special case, by moving the cursor outside of the grid. @@ -268,17 +267,14 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> { wrapped.insert(0, row[cols - 1]); let mut spacer = T::default(); - spacer.flags_mut().insert(Flags::WIDE_CHAR_SPACER); + spacer.flags_mut().insert(Flags::LEADING_WIDE_CHAR_SPACER); row[cols - 1] = spacer; } // Remove wide char spacer before shrinking. let len = wrapped.len(); - if (len == 1 || (len >= 2 && !wrapped[len - 2].flags().contains(Flags::WIDE_CHAR))) - && wrapped[len - 1].flags().contains(Flags::WIDE_CHAR_SPACER) - { + if wrapped[len - 1].flags().contains(Flags::LEADING_WIDE_CHAR_SPACER) { if len == 1 { - // Delete the wrapped content if it contains only a leading spacer. row[cols - 1].flags_mut().insert(Flags::WRAPLINE); new_raw.push(row); break; diff --git a/alacritty_terminal/src/grid/storage.rs b/alacritty_terminal/src/grid/storage.rs index 4b7ca41a..a025a99c 100644 --- a/alacritty_terminal/src/grid/storage.rs +++ b/alacritty_terminal/src/grid/storage.rs @@ -232,7 +232,9 @@ impl<T> Storage<T> { /// Rotate the grid up, moving all existing lines down in history. /// - /// This is a faster, specialized version of [`rotate`]. + /// This is a faster, specialized version of [`rotate_left`]. + /// + /// [`rotate_left`]: https://doc.rust-lang.org/std/vec/struct.Vec.html#method.rotate_left #[inline] pub fn rotate_up(&mut self, count: usize) { self.zero = (self.zero + count) % self.inner.len(); diff --git a/alacritty_terminal/src/grid/tests.rs b/alacritty_terminal/src/grid/tests.rs index dbe5f1fc..1ed279a0 100644 --- a/alacritty_terminal/src/grid/tests.rs +++ b/alacritty_terminal/src/grid/tests.rs @@ -1,7 +1,6 @@ //! Tests for the Grid. -use super::{BidirectionalIterator, Grid}; -use crate::grid::GridCell; +use super::{BidirectionalIterator, Dimensions, Grid, GridCell}; use crate::index::{Column, Line, Point}; use crate::term::cell::{Cell, Flags}; @@ -171,7 +170,7 @@ fn shrink_reflow() { grid.resize(true, Line(1), Column(2)); - assert_eq!(grid.len(), 3); + assert_eq!(grid.total_lines(), 3); assert_eq!(grid[2].len(), 2); assert_eq!(grid[2][Column(0)], cell('1')); @@ -198,7 +197,7 @@ fn shrink_reflow_twice() { grid.resize(true, Line(1), Column(4)); grid.resize(true, Line(1), Column(2)); - assert_eq!(grid.len(), 3); + assert_eq!(grid.total_lines(), 3); assert_eq!(grid[2].len(), 2); assert_eq!(grid[2][Column(0)], cell('1')); @@ -224,7 +223,7 @@ fn shrink_reflow_empty_cell_inside_line() { grid.resize(true, Line(1), Column(2)); - assert_eq!(grid.len(), 2); + assert_eq!(grid.total_lines(), 2); assert_eq!(grid[1].len(), 2); assert_eq!(grid[1][Column(0)], cell('1')); @@ -236,7 +235,7 @@ fn shrink_reflow_empty_cell_inside_line() { grid.resize(true, Line(1), Column(1)); - assert_eq!(grid.len(), 4); + assert_eq!(grid.total_lines(), 4); assert_eq!(grid[3].len(), 1); assert_eq!(grid[3][Column(0)], wrap_cell('1')); @@ -261,7 +260,7 @@ fn grow_reflow() { grid.resize(true, Line(2), Column(3)); - assert_eq!(grid.len(), 2); + assert_eq!(grid.total_lines(), 2); assert_eq!(grid[1].len(), 3); assert_eq!(grid[1][Column(0)], cell('1')); @@ -287,7 +286,7 @@ fn grow_reflow_multiline() { grid.resize(true, Line(3), Column(6)); - assert_eq!(grid.len(), 3); + assert_eq!(grid.total_lines(), 3); assert_eq!(grid[2].len(), 6); assert_eq!(grid[2][Column(0)], cell('1')); @@ -318,7 +317,7 @@ fn grow_reflow_disabled() { grid.resize(false, Line(2), Column(3)); - assert_eq!(grid.len(), 2); + assert_eq!(grid.total_lines(), 2); assert_eq!(grid[1].len(), 3); assert_eq!(grid[1][Column(0)], cell('1')); @@ -342,7 +341,7 @@ fn shrink_reflow_disabled() { grid.resize(false, Line(1), Column(2)); - assert_eq!(grid.len(), 1); + assert_eq!(grid.total_lines(), 1); assert_eq!(grid[0].len(), 2); assert_eq!(grid[0][Column(0)], cell('1')); diff --git a/alacritty_terminal/src/index.rs b/alacritty_terminal/src/index.rs index 019de83b..baed323e 100644 --- a/alacritty_terminal/src/index.rs +++ b/alacritty_terminal/src/index.rs @@ -7,16 +7,20 @@ use std::ops::{self, Add, AddAssign, Deref, Range, Sub, SubAssign}; use serde::{Deserialize, Serialize}; +use crate::grid::Dimensions; use crate::term::RenderableCell; /// The side of a cell. +pub type Side = Direction; + +/// Horizontal direction. #[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum Side { +pub enum Direction { Left, Right, } -impl Side { +impl Direction { pub fn opposite(self) -> Self { match self { Side::Right => Side::Left, @@ -25,8 +29,23 @@ impl Side { } } +/// Behavior for handling grid boundaries. +pub enum Boundary { + /// Clamp to grid boundaries. + /// + /// When an operation exceeds the grid boundaries, the last point will be returned no matter + /// how far the boundaries were exceeded. + Clamp, + + /// Wrap around grid bondaries. + /// + /// When an operation exceeds the grid boundaries, the point will wrap around the entire grid + /// history and continue at the other side. + Wrap, +} + /// Index in the grid using row, column notation. -#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize, PartialOrd)] +#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize)] pub struct Point<L = Line> { pub line: L, pub col: Column, @@ -65,43 +84,84 @@ impl<L> Point<L> { self.col = Column((self.col.0 + rhs) % num_cols); self } +} +impl Point<usize> { #[inline] #[must_use = "this returns the result of the operation, without modifying the original"] - pub fn sub_absolute(mut self, num_cols: Column, rhs: usize) -> Point<L> + pub fn sub_absolute<D>(mut self, dimensions: &D, boundary: Boundary, rhs: usize) -> Point<usize> where - L: Copy + Default + Into<Line> + Add<usize, Output = L> + Sub<usize, Output = L>, + D: Dimensions, { - let num_cols = num_cols.0; - self.line = self.line + ((rhs + num_cols - 1).saturating_sub(self.col.0) / num_cols); + let total_lines = dimensions.total_lines(); + let num_cols = dimensions.cols().0; + + self.line += (rhs + num_cols - 1).saturating_sub(self.col.0) / num_cols; self.col = Column((num_cols + self.col.0 - rhs % num_cols) % num_cols); - self + + if self.line >= total_lines { + match boundary { + Boundary::Clamp => Point::new(total_lines - 1, Column(0)), + Boundary::Wrap => Point::new(self.line - total_lines, self.col), + } + } else { + self + } } #[inline] #[must_use = "this returns the result of the operation, without modifying the original"] - pub fn add_absolute(mut self, num_cols: Column, rhs: usize) -> Point<L> + pub fn add_absolute<D>(mut self, dimensions: &D, boundary: Boundary, rhs: usize) -> Point<usize> where - L: Copy + Default + Into<Line> + Add<usize, Output = L> + Sub<usize, Output = L>, + D: Dimensions, { - let line_changes = (rhs + self.col.0) / num_cols.0; - if self.line.into() >= Line(line_changes) { - self.line = self.line - line_changes; + let num_cols = dimensions.cols(); + + let line_delta = (rhs + self.col.0) / num_cols.0; + + if self.line >= line_delta { + self.line -= line_delta; self.col = Column((self.col.0 + rhs) % num_cols.0); self } else { - Point::new(L::default(), num_cols - 1) + match boundary { + Boundary::Clamp => Point::new(0, num_cols - 1), + Boundary::Wrap => { + let col = Column((self.col.0 + rhs) % num_cols.0); + let line = dimensions.total_lines() + self.line - line_delta; + Point::new(line, col) + }, + } } } } +impl PartialOrd for Point { + fn partial_cmp(&self, other: &Point) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + impl Ord for Point { fn cmp(&self, other: &Point) -> Ordering { match (self.line.cmp(&other.line), self.col.cmp(&other.col)) { - (Ordering::Equal, Ordering::Equal) => Ordering::Equal, - (Ordering::Equal, ord) | (ord, Ordering::Equal) => ord, - (Ordering::Less, _) => Ordering::Less, - (Ordering::Greater, _) => Ordering::Greater, + (Ordering::Equal, ord) | (ord, _) => ord, + } + } +} + +impl PartialOrd for Point<usize> { + fn partial_cmp(&self, other: &Point<usize>) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl Ord for Point<usize> { + fn cmp(&self, other: &Point<usize>) -> Ordering { + match (self.line.cmp(&other.line), self.col.cmp(&other.col)) { + (Ordering::Equal, ord) => ord, + (Ordering::Less, _) => Ordering::Greater, + (Ordering::Greater, _) => Ordering::Less, } } } @@ -429,7 +489,7 @@ ops!(Linear, Linear); #[cfg(test)] mod tests { - use super::{Column, Line, Point}; + use super::*; #[test] fn location_ordering() { @@ -493,51 +553,100 @@ mod tests { #[test] fn add_absolute() { - let num_cols = Column(42); let point = Point::new(0, Column(13)); - let result = point.add_absolute(num_cols, 1); + let result = point.add_absolute(&(Line(1), Column(42)), Boundary::Clamp, 1); assert_eq!(result, Point::new(0, point.col + 1)); } #[test] - fn add_absolute_wrap() { - let num_cols = Column(42); - let point = Point::new(1, num_cols - 1); + fn add_absolute_wrapline() { + let point = Point::new(1, Column(41)); - let result = point.add_absolute(num_cols, 1); + let result = point.add_absolute(&(Line(2), Column(42)), Boundary::Clamp, 1); + + assert_eq!(result, Point::new(0, Column(0))); + } + + #[test] + fn add_absolute_multiline_wrapline() { + let point = Point::new(2, Column(9)); + + let result = point.add_absolute(&(Line(3), Column(10)), Boundary::Clamp, 11); assert_eq!(result, Point::new(0, Column(0))); } #[test] fn add_absolute_clamp() { - let num_cols = Column(42); - let point = Point::new(0, num_cols - 1); + let point = Point::new(0, Column(41)); - let result = point.add_absolute(num_cols, 1); + let result = point.add_absolute(&(Line(1), Column(42)), Boundary::Clamp, 1); assert_eq!(result, point); } #[test] + fn add_absolute_wrap() { + let point = Point::new(0, Column(41)); + + let result = point.add_absolute(&(Line(3), Column(42)), Boundary::Wrap, 1); + + assert_eq!(result, Point::new(2, Column(0))); + } + + #[test] + fn add_absolute_multiline_wrap() { + let point = Point::new(0, Column(9)); + + let result = point.add_absolute(&(Line(3), Column(10)), Boundary::Wrap, 11); + + assert_eq!(result, Point::new(1, Column(0))); + } + + #[test] fn sub_absolute() { - let num_cols = Column(42); let point = Point::new(0, Column(13)); - let result = point.sub_absolute(num_cols, 1); + let result = point.sub_absolute(&(Line(1), Column(42)), Boundary::Clamp, 1); assert_eq!(result, Point::new(0, point.col - 1)); } #[test] - fn sub_absolute_wrap() { - let num_cols = Column(42); + fn sub_absolute_wrapline() { let point = Point::new(0, Column(0)); - let result = point.sub_absolute(num_cols, 1); + let result = point.sub_absolute(&(Line(2), Column(42)), Boundary::Clamp, 1); + + assert_eq!(result, Point::new(1, Column(41))); + } + + #[test] + fn sub_absolute_multiline_wrapline() { + let point = Point::new(0, Column(0)); + + let result = point.sub_absolute(&(Line(3), Column(10)), Boundary::Clamp, 11); + + assert_eq!(result, Point::new(2, Column(9))); + } + + #[test] + fn sub_absolute_wrap() { + let point = Point::new(2, Column(0)); + + let result = point.sub_absolute(&(Line(3), Column(42)), Boundary::Wrap, 1); + + assert_eq!(result, Point::new(0, Column(41))); + } + + #[test] + fn sub_absolute_multiline_wrap() { + let point = Point::new(2, Column(0)); + + let result = point.sub_absolute(&(Line(3), Column(10)), Boundary::Wrap, 11); - assert_eq!(result, Point::new(1, num_cols - 1)); + assert_eq!(result, Point::new(1, Column(9))); } } diff --git a/alacritty_terminal/src/selection.rs b/alacritty_terminal/src/selection.rs index dbd11592..83dea824 100644 --- a/alacritty_terminal/src/selection.rs +++ b/alacritty_terminal/src/selection.rs @@ -9,8 +9,9 @@ use std::convert::TryFrom; use std::mem; use std::ops::{Bound, Range, RangeBounds}; +use crate::grid::Dimensions; use crate::index::{Column, Line, Point, Side}; -use crate::term::{Search, Term}; +use crate::term::Term; /// A Point and side within that point. #[derive(Debug, Copy, Clone, PartialEq)] @@ -98,20 +99,19 @@ impl Selection { self.region.end = Anchor::new(point, side); } - pub fn rotate( + pub fn rotate<D: Dimensions>( mut self, - num_lines: Line, - num_cols: Column, + dimensions: &D, range: &Range<Line>, delta: isize, ) -> Option<Selection> { + let num_lines = dimensions.screen_lines().0; + let num_cols = dimensions.cols().0; let range_bottom = range.start.0; let range_top = range.end.0; - let num_lines = num_lines.0; - let num_cols = num_cols.0; let (mut start, mut end) = (&mut self.region.start, &mut self.region.end); - if Self::points_need_swap(start.point, end.point) { + if Selection::points_need_swap(start.point, end.point) { mem::swap(&mut start, &mut end); } @@ -238,7 +238,7 @@ impl Selection { /// Convert selection to grid coordinates. pub fn to_range<T>(&self, term: &Term<T>) -> Option<SelectionRange> { let grid = term.grid(); - let num_cols = grid.num_cols(); + let num_cols = grid.cols(); // Order start above the end. let mut start = self.region.start; @@ -250,7 +250,7 @@ impl Selection { // Clamp to inside the grid buffer. let is_block = self.ty == SelectionType::Block; - let (start, end) = Self::grid_clamp(start, end, is_block, grid.len()).ok()?; + let (start, end) = Self::grid_clamp(start, end, is_block, grid.total_lines()).ok()?; match self.ty { SelectionType::Simple => self.range_simple(start, end, num_cols), @@ -408,7 +408,7 @@ mod tests { fn send_event(&self, _event: Event) {} } - fn term(width: usize, height: usize) -> Term<Mock> { + fn term(height: usize, width: usize) -> Term<Mock> { let size = SizeInfo { width: width as f32, height: height as f32, @@ -468,7 +468,7 @@ mod tests { Selection::new(SelectionType::Simple, Point::new(0, Column(0)), Side::Right); selection.update(Point::new(0, Column(1)), Side::Left); - assert_eq!(selection.to_range(&term(2, 1)), None); + assert_eq!(selection.to_range(&term(1, 2)), None); } /// Test adjacent cell selection from right to left. @@ -482,7 +482,7 @@ mod tests { Selection::new(SelectionType::Simple, Point::new(0, Column(1)), Side::Left); selection.update(Point::new(0, Column(0)), Side::Right); - assert_eq!(selection.to_range(&term(2, 1)), None); + assert_eq!(selection.to_range(&term(1, 2)), None); } /// Test selection across adjacent lines. @@ -499,7 +499,7 @@ mod tests { Selection::new(SelectionType::Simple, Point::new(1, Column(1)), Side::Right); selection.update(Point::new(0, Column(1)), Side::Right); - assert_eq!(selection.to_range(&term(5, 2)).unwrap(), SelectionRange { + assert_eq!(selection.to_range(&term(2, 5)).unwrap(), SelectionRange { start: Point::new(1, Column(2)), end: Point::new(0, Column(1)), is_block: false, @@ -523,7 +523,7 @@ mod tests { selection.update(Point::new(1, Column(1)), Side::Right); selection.update(Point::new(1, Column(0)), Side::Right); - assert_eq!(selection.to_range(&term(5, 2)).unwrap(), SelectionRange { + assert_eq!(selection.to_range(&term(2, 5)).unwrap(), SelectionRange { start: Point::new(1, Column(1)), end: Point::new(0, Column(1)), is_block: false, @@ -532,14 +532,13 @@ mod tests { #[test] fn line_selection() { - let num_lines = Line(10); - let num_cols = Column(5); + let size = (Line(10), Column(5)); let mut selection = Selection::new(SelectionType::Lines, Point::new(0, Column(1)), Side::Left); selection.update(Point::new(5, Column(1)), Side::Right); - selection = selection.rotate(num_lines, num_cols, &(Line(0)..num_lines), 7).unwrap(); + selection = selection.rotate(&size, &(Line(0)..size.0), 7).unwrap(); - assert_eq!(selection.to_range(&term(num_cols.0, num_lines.0)).unwrap(), SelectionRange { + assert_eq!(selection.to_range(&term(*size.0, *size.1)).unwrap(), SelectionRange { start: Point::new(9, Column(0)), end: Point::new(7, Column(4)), is_block: false, @@ -548,14 +547,13 @@ mod tests { #[test] fn semantic_selection() { - let num_lines = Line(10); - let num_cols = Column(5); + let size = (Line(10), Column(5)); let mut selection = Selection::new(SelectionType::Semantic, Point::new(0, Column(3)), Side::Left); selection.update(Point::new(5, Column(1)), Side::Right); - selection = selection.rotate(num_lines, num_cols, &(Line(0)..num_lines), 7).unwrap(); + selection = selection.rotate(&size, &(Line(0)..size.0), 7).unwrap(); - assert_eq!(selection.to_range(&term(num_cols.0, num_lines.0)).unwrap(), SelectionRange { + assert_eq!(selection.to_range(&term(*size.0, *size.1)).unwrap(), SelectionRange { start: Point::new(9, Column(0)), end: Point::new(7, Column(3)), is_block: false, @@ -564,14 +562,13 @@ mod tests { #[test] fn simple_selection() { - let num_lines = Line(10); - let num_cols = Column(5); + let size = (Line(10), Column(5)); let mut selection = Selection::new(SelectionType::Simple, Point::new(0, Column(3)), Side::Right); selection.update(Point::new(5, Column(1)), Side::Right); - selection = selection.rotate(num_lines, num_cols, &(Line(0)..num_lines), 7).unwrap(); + selection = selection.rotate(&size, &(Line(0)..size.0), 7).unwrap(); - assert_eq!(selection.to_range(&term(num_cols.0, num_lines.0)).unwrap(), SelectionRange { + assert_eq!(selection.to_range(&term(*size.0, *size.1)).unwrap(), SelectionRange { start: Point::new(9, Column(0)), end: Point::new(7, Column(3)), is_block: false, @@ -580,14 +577,13 @@ mod tests { #[test] fn block_selection() { - let num_lines = Line(10); - let num_cols = Column(5); + let size = (Line(10), Column(5)); let mut selection = Selection::new(SelectionType::Block, Point::new(0, Column(3)), Side::Right); selection.update(Point::new(5, Column(1)), Side::Right); - selection = selection.rotate(num_lines, num_cols, &(Line(0)..num_lines), 7).unwrap(); + selection = selection.rotate(&size, &(Line(0)..size.0), 7).unwrap(); - assert_eq!(selection.to_range(&term(num_cols.0, num_lines.0)).unwrap(), SelectionRange { + assert_eq!(selection.to_range(&term(*size.0, *size.1)).unwrap(), SelectionRange { start: Point::new(9, Column(2)), end: Point::new(7, Column(3)), is_block: true @@ -624,14 +620,13 @@ mod tests { #[test] fn rotate_in_region_up() { - let num_lines = Line(10); - let num_cols = Column(5); + let size = (Line(10), Column(5)); let mut selection = Selection::new(SelectionType::Simple, Point::new(2, Column(3)), Side::Right); selection.update(Point::new(5, Column(1)), Side::Right); - selection = selection.rotate(num_lines, num_cols, &(Line(1)..(num_lines - 1)), 4).unwrap(); + selection = selection.rotate(&size, &(Line(1)..(size.0 - 1)), 4).unwrap(); - assert_eq!(selection.to_range(&term(num_cols.0, num_lines.0)).unwrap(), SelectionRange { + assert_eq!(selection.to_range(&term(*size.0, *size.1)).unwrap(), SelectionRange { start: Point::new(8, Column(0)), end: Point::new(6, Column(3)), is_block: false, @@ -640,30 +635,28 @@ mod tests { #[test] fn rotate_in_region_down() { - let num_lines = Line(10); - let num_cols = Column(5); + let size = (Line(10), Column(5)); let mut selection = Selection::new(SelectionType::Simple, Point::new(5, Column(3)), Side::Right); selection.update(Point::new(8, Column(1)), Side::Left); - selection = selection.rotate(num_lines, num_cols, &(Line(1)..(num_lines - 1)), -5).unwrap(); + selection = selection.rotate(&size, &(Line(1)..(size.0 - 1)), -5).unwrap(); - assert_eq!(selection.to_range(&term(num_cols.0, num_lines.0)).unwrap(), SelectionRange { + assert_eq!(selection.to_range(&term(*size.0, *size.1)).unwrap(), SelectionRange { start: Point::new(3, Column(1)), - end: Point::new(1, num_cols - 1), + end: Point::new(1, size.1 - 1), is_block: false, }); } #[test] fn rotate_in_region_up_block() { - let num_lines = Line(10); - let num_cols = Column(5); + let size = (Line(10), Column(5)); let mut selection = Selection::new(SelectionType::Block, Point::new(2, Column(3)), Side::Right); selection.update(Point::new(5, Column(1)), Side::Right); - selection = selection.rotate(num_lines, num_cols, &(Line(1)..(num_lines - 1)), 4).unwrap(); + selection = selection.rotate(&size, &(Line(1)..(size.0 - 1)), 4).unwrap(); - assert_eq!(selection.to_range(&term(num_cols.0, num_lines.0)).unwrap(), SelectionRange { + assert_eq!(selection.to_range(&term(*size.0, *size.1)).unwrap(), SelectionRange { start: Point::new(8, Column(2)), end: Point::new(6, Column(3)), is_block: true, diff --git a/alacritty_terminal/src/term/cell.rs b/alacritty_terminal/src/term/cell.rs index 5f948b19..3fdd8cea 100644 --- a/alacritty_terminal/src/term/cell.rs +++ b/alacritty_terminal/src/term/cell.rs @@ -12,18 +12,19 @@ pub const MAX_ZEROWIDTH_CHARS: usize = 5; bitflags! { #[derive(Serialize, Deserialize)] pub struct Flags: u16 { - const INVERSE = 0b00_0000_0001; - const BOLD = 0b00_0000_0010; - const ITALIC = 0b00_0000_0100; - const BOLD_ITALIC = 0b00_0000_0110; - const UNDERLINE = 0b00_0000_1000; - const WRAPLINE = 0b00_0001_0000; - const WIDE_CHAR = 0b00_0010_0000; - const WIDE_CHAR_SPACER = 0b00_0100_0000; - const DIM = 0b00_1000_0000; - const DIM_BOLD = 0b00_1000_0010; - const HIDDEN = 0b01_0000_0000; - const STRIKEOUT = 0b10_0000_0000; + const INVERSE = 0b000_0000_0001; + const BOLD = 0b000_0000_0010; + const ITALIC = 0b000_0000_0100; + const BOLD_ITALIC = 0b000_0000_0110; + const UNDERLINE = 0b000_0000_1000; + const WRAPLINE = 0b000_0001_0000; + const WIDE_CHAR = 0b000_0010_0000; + const WIDE_CHAR_SPACER = 0b000_0100_0000; + const DIM = 0b000_1000_0000; + const DIM_BOLD = 0b000_1000_0010; + const HIDDEN = 0b001_0000_0000; + const STRIKEOUT = 0b010_0000_0000; + const LEADING_WIDE_CHAR_SPACER = 0b100_0000_0000; } } @@ -59,7 +60,8 @@ impl GridCell for Cell { | Flags::UNDERLINE | Flags::STRIKEOUT | Flags::WRAPLINE - | Flags::WIDE_CHAR_SPACER, + | Flags::WIDE_CHAR_SPACER + | Flags::LEADING_WIDE_CHAR_SPACER, ) } diff --git a/alacritty_terminal/src/term/color.rs b/alacritty_terminal/src/term/color.rs index ef2c2402..f20601d6 100644 --- a/alacritty_terminal/src/term/color.rs +++ b/alacritty_terminal/src/term/color.rs @@ -2,12 +2,13 @@ use std::fmt; use std::ops::{Index, IndexMut, Mul}; use std::str::FromStr; -use log::{error, trace}; -use serde::de::Visitor; +use log::trace; +use serde::de::{Error as _, Visitor}; use serde::{Deserialize, Deserializer, Serialize}; +use serde_yaml::Value; use crate::ansi; -use crate::config::{Colors, LOG_TARGET_CONFIG}; +use crate::config::Colors; pub const COUNT: usize = 269; @@ -67,7 +68,7 @@ impl<'de> Deserialize<'de> for Rgb { f.write_str("hex color like #ff00ff") } - fn visit_str<E>(self, value: &str) -> ::std::result::Result<Rgb, E> + fn visit_str<E>(self, value: &str) -> Result<Rgb, E> where E: serde::de::Error, { @@ -81,7 +82,7 @@ impl<'de> Deserialize<'de> for Rgb { } // Return an error if the syntax is incorrect. - let value = serde_yaml::Value::deserialize(deserializer)?; + let value = Value::deserialize(deserializer)?; // Attempt to deserialize from struct form. if let Ok(RgbDerivedDeser { r, g, b }) = RgbDerivedDeser::deserialize(value.clone()) { @@ -89,23 +90,14 @@ impl<'de> Deserialize<'de> for Rgb { } // Deserialize from hex notation (either 0xff00ff or #ff00ff). - match value.deserialize_str(RgbVisitor) { - Ok(rgb) => Ok(rgb), - Err(err) => { - error!( - target: LOG_TARGET_CONFIG, - "Problem with config: {}; using color #000000", err - ); - Ok(Rgb::default()) - }, - } + value.deserialize_str(RgbVisitor).map_err(D::Error::custom) } } impl FromStr for Rgb { type Err = (); - fn from_str(s: &str) -> std::result::Result<Rgb, ()> { + fn from_str(s: &str) -> Result<Rgb, ()> { let chars = if s.starts_with("0x") && s.len() == 8 { &s[2..] } else if s.starts_with('#') && s.len() == 7 { @@ -128,6 +120,66 @@ impl FromStr for Rgb { } } +/// RGB color optionally referencing the cell's foreground or background. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum CellRgb { + CellForeground, + CellBackground, + Rgb(Rgb), +} + +impl CellRgb { + pub fn color(self, foreground: Rgb, background: Rgb) -> Rgb { + match self { + Self::CellForeground => foreground, + Self::CellBackground => background, + Self::Rgb(rgb) => rgb, + } + } +} + +impl Default for CellRgb { + fn default() -> Self { + Self::Rgb(Rgb::default()) + } +} + +impl<'de> Deserialize<'de> for CellRgb { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + const EXPECTING: &str = "CellForeground, CellBackground, or hex color like #ff00ff"; + + struct CellRgbVisitor; + impl<'a> Visitor<'a> for CellRgbVisitor { + type Value = CellRgb; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(EXPECTING) + } + + fn visit_str<E>(self, value: &str) -> Result<CellRgb, E> + where + E: serde::de::Error, + { + // Attempt to deserialize as enum constants. + match value { + "CellForeground" => return Ok(CellRgb::CellForeground), + "CellBackground" => return Ok(CellRgb::CellBackground), + _ => (), + } + + Rgb::from_str(&value[..]).map(CellRgb::Rgb).map_err(|_| { + E::custom(format!("failed to parse color {}; expected {}", value, EXPECTING)) + }) + } + } + + deserializer.deserialize_str(CellRgbVisitor).map_err(D::Error::custom) + } +} + /// List of indexed colors. /// /// The first 16 entries are the standard ansi named colors. Items 16..232 are @@ -179,9 +231,6 @@ impl List { self[ansi::NamedColor::Foreground] = colors.primary.foreground; self[ansi::NamedColor::Background] = colors.primary.background; - // Background for custom cursor colors. - self[ansi::NamedColor::Cursor] = colors.cursor.cursor.unwrap_or_else(Rgb::default); - // Dims. self[ansi::NamedColor::DimForeground] = colors.primary.dim_foreground.unwrap_or(colors.primary.foreground * DIM_FACTOR); diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs index 996f6809..d59838d4 100644 --- a/alacritty_terminal/src/term/mod.rs +++ b/alacritty_terminal/src/term/mod.rs @@ -1,195 +1,121 @@ //! Exports the `Term` type which is a high-level API for the Grid. use std::cmp::{max, min}; -use std::ops::{Index, IndexMut, Range}; +use std::iter::Peekable; +use std::ops::{Index, IndexMut, Range, RangeInclusive}; use std::sync::Arc; use std::time::{Duration, Instant}; -use std::{io, mem, ptr, str}; +use std::{io, iter, mem, ptr, str}; use log::{debug, trace}; use serde::{Deserialize, Serialize}; use unicode_width::UnicodeWidthChar; use crate::ansi::{ - self, Attr, CharsetIndex, Color, CursorStyle, Handler, NamedColor, StandardCharset, TermInfo, + self, Attr, CharsetIndex, Color, CursorStyle, Handler, NamedColor, StandardCharset, }; use crate::config::{Config, VisualBellAnimation}; use crate::event::{Event, EventListener}; -use crate::grid::{ - BidirectionalIterator, DisplayIter, Grid, GridCell, IndexRegion, Indexed, Scroll, -}; -use crate::index::{self, Column, IndexRange, Line, Point, Side}; +use crate::grid::{Dimensions, DisplayIter, Grid, IndexRegion, Indexed, Scroll}; +use crate::index::{self, Boundary, Column, Direction, IndexRange, Line, Point, Side}; use crate::selection::{Selection, SelectionRange}; use crate::term::cell::{Cell, Flags, LineLength}; -use crate::term::color::{Rgb, DIM_FACTOR}; +use crate::term::color::{CellRgb, Rgb, DIM_FACTOR}; +use crate::term::search::{RegexIter, RegexSearch}; use crate::vi_mode::{ViModeCursor, ViMotion}; pub mod cell; pub mod color; - -/// Used to match equal brackets, when performing a bracket-pair selection. -const BRACKET_PAIRS: [(char, char); 4] = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')]; +mod search; /// Max size of the window title stack. const TITLE_STACK_MAX_DEPTH: usize = 4096; +/// Maximum number of linewraps followed outside of the viewport during search highlighting. +const MAX_SEARCH_LINES: usize = 100; + /// Default tab interval, corresponding to terminfo `it` value. const INITIAL_TABSTOPS: usize = 8; /// Minimum number of columns and lines. const MIN_SIZE: usize = 2; -/// A type that can expand a given point to a region. -/// -/// Usually this is implemented for some 2-D array type since -/// points are two dimensional indices. -pub trait Search { - /// Find the nearest semantic boundary _to the left_ of provided point. - fn semantic_search_left(&self, _: Point<usize>) -> Point<usize>; - /// Find the nearest semantic boundary _to the point_ of provided point. - fn semantic_search_right(&self, _: Point<usize>) -> Point<usize>; - /// Find the beginning of a line, following line wraps. - fn line_search_left(&self, _: Point<usize>) -> Point<usize>; - /// Find the end of a line, following line wraps. - fn line_search_right(&self, _: Point<usize>) -> Point<usize>; - /// Find the nearest matching bracket. - fn bracket_search(&self, _: Point<usize>) -> Option<Point<usize>>; +/// Cursor storing all information relevant for rendering. +#[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize)] +struct RenderableCursor { + text_color: CellRgb, + cursor_color: CellRgb, + key: CursorKey, + point: Point, + rendered: bool, } -impl<T> Search for Term<T> { - fn semantic_search_left(&self, mut point: Point<usize>) -> Point<usize> { - // Limit the starting point to the last line in the history. - point.line = min(point.line, self.grid.len() - 1); - - let mut iter = self.grid.iter_from(point); - let last_col = self.grid.num_cols() - Column(1); - - while let Some(cell) = iter.prev() { - if !cell.flags.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER) - && self.semantic_escape_chars.contains(cell.c) - { - break; - } - - if iter.point().col == last_col && !cell.flags.contains(Flags::WRAPLINE) { - // Cut off if on new line or hit escape char. - break; - } - - point = iter.point(); - } - - point - } - - fn semantic_search_right(&self, mut point: Point<usize>) -> Point<usize> { - // Limit the starting point to the last line in the history. - point.line = min(point.line, self.grid.len() - 1); - - let mut iter = self.grid.iter_from(point); - let last_col = self.grid.num_cols() - 1; +/// A key for caching cursor glyphs. +#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Deserialize)] +pub struct CursorKey { + pub style: CursorStyle, + pub is_wide: bool, +} - while let Some(cell) = iter.next() { - if !cell.flags.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER) - && self.semantic_escape_chars.contains(cell.c) - { - break; - } +type MatchIter<'a> = Box<dyn Iterator<Item = RangeInclusive<Point<usize>>> + 'a>; - point = iter.point(); +/// Regex search highlight tracking. +pub struct RenderableSearch<'a> { + iter: Peekable<MatchIter<'a>>, +} - if point.col == last_col && !cell.flags.contains(Flags::WRAPLINE) { - // Cut off if on new line or hit escape char. - break; +impl<'a> RenderableSearch<'a> { + /// Create a new renderable search iterator. + fn new<T>(term: &'a Term<T>) -> Self { + let viewport_end = term.grid().display_offset(); + let viewport_start = viewport_end + term.grid().screen_lines().0 - 1; + + // Compute start of the first and end of the last line. + let start_point = Point::new(viewport_start, Column(0)); + let mut start = term.line_search_left(start_point); + let end_point = Point::new(viewport_end, term.grid().cols() - 1); + let mut end = term.line_search_right(end_point); + + // Set upper bound on search before/after the viewport to prevent excessive blocking. + if start.line > viewport_start + MAX_SEARCH_LINES { + if start.line == 0 { + // Do not highlight anything if this line is the last. + let iter: MatchIter<'a> = Box::new(iter::empty()); + return Self { iter: iter.peekable() }; + } else { + // Start at next line if this one is too long. + start.line -= 1; } } + end.line = max(end.line, viewport_end.saturating_sub(MAX_SEARCH_LINES)); - point - } - - fn line_search_left(&self, mut point: Point<usize>) -> Point<usize> { - while point.line + 1 < self.grid.len() - && self.grid[point.line + 1][self.grid.num_cols() - 1].flags.contains(Flags::WRAPLINE) - { - point.line += 1; - } - - point.col = Column(0); + // Create an iterater for the current regex search for all visible matches. + let iter: MatchIter<'a> = Box::new( + RegexIter::new(start, end, Direction::Right, &term) + .skip_while(move |rm| rm.end().line > viewport_start) + .take_while(move |rm| rm.start().line >= viewport_end), + ); - point + Self { iter: iter.peekable() } } - fn line_search_right(&self, mut point: Point<usize>) -> Point<usize> { - while self.grid[point.line][self.grid.num_cols() - 1].flags.contains(Flags::WRAPLINE) { - point.line -= 1; - } - - point.col = self.grid.num_cols() - 1; - - point - } - - fn bracket_search(&self, point: Point<usize>) -> Option<Point<usize>> { - let start_char = self.grid[point.line][point.col].c; - - // Find the matching bracket we're looking for. - let (forwards, end_char) = BRACKET_PAIRS.iter().find_map(|(open, close)| { - if open == &start_char { - Some((true, *close)) - } else if close == &start_char { - Some((false, *open)) + /// Advance the search tracker to the next point. + /// + /// This will return `true` if the point passed is part of a search match. + fn advance(&mut self, point: Point<usize>) -> bool { + while let Some(regex_match) = &self.iter.peek() { + if regex_match.start() > &point { + break; + } else if regex_match.end() < &point { + let _ = self.iter.next(); } else { - None - } - })?; - - let mut iter = self.grid.iter_from(point); - - // For every character match that equals the starting bracket, we - // ignore one bracket of the opposite type. - let mut skip_pairs = 0; - - loop { - // Check the next cell. - let cell = if forwards { iter.next() } else { iter.prev() }; - - // Break if there are no more cells. - let c = match cell { - Some(cell) => cell.c, - None => break, - }; - - // Check if the bracket matches. - if c == end_char && skip_pairs == 0 { - return Some(iter.point()); - } else if c == start_char { - skip_pairs += 1; - } else if c == end_char { - skip_pairs -= 1; + return true; } } - - None + false } } -/// Cursor storing all information relevant for rendering. -#[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize)] -struct RenderableCursor { - text_color: Option<Rgb>, - cursor_color: Option<Rgb>, - key: CursorKey, - point: Point, - rendered: bool, -} - -/// A key for caching cursor glyphs. -#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Deserialize)] -pub struct CursorKey { - pub style: CursorStyle, - pub is_wide: bool, -} - /// Iterator that yields cells needing render. /// /// Yields cells that require work to be displayed (that is, not a an empty @@ -205,6 +131,7 @@ pub struct RenderableCellsIter<'a, C> { config: &'a Config<C>, colors: &'a color::List, selection: Option<SelectionRange<Line>>, + search: RenderableSearch<'a>, } impl<'a, C> RenderableCellsIter<'a, C> { @@ -212,26 +139,24 @@ impl<'a, C> RenderableCellsIter<'a, C> { /// /// The cursor and terminal mode are required for properly displaying the /// cursor. - fn new<'b, T>( - term: &'b Term<T>, - config: &'b Config<C>, + fn new<T>( + term: &'a Term<T>, + config: &'a Config<C>, selection: Option<SelectionRange>, - ) -> RenderableCellsIter<'b, C> { + ) -> RenderableCellsIter<'a, C> { let grid = &term.grid; - let inner = grid.display_iter(); - let selection_range = selection.and_then(|span| { let (limit_start, limit_end) = if span.is_block { (span.start.col, span.end.col) } else { - (Column(0), grid.num_cols() - 1) + (Column(0), grid.cols() - 1) }; // Do not render completely offscreen selection. - let viewport_start = grid.display_offset(); - let viewport_end = viewport_start + grid.num_lines().0; - if span.end.line >= viewport_end || span.start.line < viewport_start { + let viewport_end = grid.display_offset(); + let viewport_start = viewport_end + grid.screen_lines().0 - 1; + if span.end.line > viewport_start || span.start.line < viewport_end { return None; } @@ -249,10 +174,11 @@ impl<'a, C> RenderableCellsIter<'a, C> { RenderableCellsIter { cursor: term.renderable_cursor(config), grid, - inner, + inner: grid.display_iter(), selection: selection_range, config, colors: &term.colors, + search: RenderableSearch::new(term), } } @@ -280,20 +206,18 @@ impl<'a, C> RenderableCellsIter<'a, C> { return true; } - let num_cols = self.grid.num_cols(); + let num_cols = self.grid.cols(); let cell = self.grid[&point]; // Check if wide char's spacers are selected. if cell.flags.contains(Flags::WIDE_CHAR) { - let prevprev = point.sub(num_cols, 2); let prev = point.sub(num_cols, 1); let next = point.add(num_cols, 1); // Check trailing spacer. selection.contains(next.col, next.line) // Check line-wrapping, leading spacer. - || (self.grid[&prev].flags.contains(Flags::WIDE_CHAR_SPACER) - && !self.grid[&prevprev].flags.contains(Flags::WIDE_CHAR) + || (self.grid[&prev].flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) && selection.contains(prev.col, prev.line)) } else if cell.flags.contains(Flags::WIDE_CHAR_SPACER) { // Check if spacer's wide char is selected. @@ -312,7 +236,7 @@ impl<'a, C> RenderableCellsIter<'a, C> { } } -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum RenderableCellContent { Chars([char; cell::MAX_ZEROWIDTH_CHARS + 1]), Cursor(CursorKey), @@ -331,38 +255,44 @@ pub struct RenderableCell { } impl RenderableCell { - fn new<C>( - config: &Config<C>, - colors: &color::List, - cell: Indexed<Cell>, - selected: bool, - ) -> Self { + fn new<'a, C>(iter: &mut RenderableCellsIter<'a, C>, cell: Indexed<Cell>) -> Self { + let point = Point::new(cell.line, cell.column); + // Lookup RGB values. - let mut fg_rgb = Self::compute_fg_rgb(config, colors, cell.fg, cell.flags); - let mut bg_rgb = Self::compute_bg_rgb(colors, cell.bg); - let mut bg_alpha = Self::compute_bg_alpha(cell.bg); - - let selection_background = config.colors.selection.background; - if let (true, Some(col)) = (selected, selection_background) { - // Override selection background with config colors. - bg_rgb = col; - bg_alpha = 1.0; - } else if selected ^ cell.inverse() { + let mut fg_rgb = Self::compute_fg_rgb(iter.config, iter.colors, cell.fg, cell.flags); + let mut bg_rgb = Self::compute_bg_rgb(iter.colors, cell.bg); + + let mut bg_alpha = if cell.inverse() { + mem::swap(&mut fg_rgb, &mut bg_rgb); + 1.0 + } else { + Self::compute_bg_alpha(cell.bg) + }; + + if iter.is_selected(point) { + let config_bg = iter.config.colors.selection.background(); + let selected_fg = iter.config.colors.selection.text().color(fg_rgb, bg_rgb); + bg_rgb = config_bg.color(fg_rgb, bg_rgb); + fg_rgb = selected_fg; + if fg_rgb == bg_rgb && !cell.flags.contains(Flags::HIDDEN) { // Reveal inversed text when fg/bg is the same. - fg_rgb = colors[NamedColor::Background]; - bg_rgb = colors[NamedColor::Foreground]; - } else { - // Invert cell fg and bg colors. - mem::swap(&mut fg_rgb, &mut bg_rgb); + fg_rgb = iter.colors[NamedColor::Background]; + bg_rgb = iter.colors[NamedColor::Foreground]; + bg_alpha = 1.0; + } else if config_bg != CellRgb::CellBackground { + bg_alpha = 1.0; + } + } else if iter.search.advance(iter.grid.visible_to_buffer(point)) { + // Highlight the cell if it is part of a search match. + let config_bg = iter.config.colors.search.matches.background; + let matched_fg = iter.config.colors.search.matches.foreground.color(fg_rgb, bg_rgb); + bg_rgb = config_bg.color(fg_rgb, bg_rgb); + fg_rgb = matched_fg; + + if config_bg != CellRgb::CellBackground { + bg_alpha = 1.0; } - - bg_alpha = 1.0; - } - - // Override selection text with config colors. - if let (true, Some(col)) = (selected, config.colors.selection.text) { - fg_rgb = col; } RenderableCell { @@ -376,6 +306,12 @@ impl RenderableCell { } } + fn is_empty(&self) -> bool { + self.bg_alpha == 0. + && !self.flags.intersects(Flags::UNDERLINE | Flags::STRIKEOUT) + && self.inner == RenderableCellContent::Chars([' '; cell::MAX_ZEROWIDTH_CHARS + 1]) + } + fn compute_fg_rgb<C>(config: &Config<C>, colors: &color::List, fg: Color, flags: Flags) -> Rgb { match fg { Color::Spec(rgb) => match flags & Flags::DIM { @@ -416,6 +352,11 @@ impl RenderableCell { } } + /// Compute background alpha based on cell's original color. + /// + /// Since an RGB color matching the background should not be transparent, this is computed + /// using the named input color, rather than checking the RGB of the background after its color + /// is computed. #[inline] fn compute_bg_alpha(bg: Color) -> f32 { if bg == Color::Named(NamedColor::Background) { @@ -448,19 +389,13 @@ impl<'a, C> Iterator for RenderableCellsIter<'a, C> { if self.cursor.point.line == self.inner.line() && self.cursor.point.col == self.inner.column() { - let selected = self.is_selected(self.cursor.point); - // Handle cell below cursor. if self.cursor.rendered { - let mut cell = - RenderableCell::new(self.config, self.colors, self.inner.next()?, selected); + let cell = self.inner.next()?; + let mut cell = RenderableCell::new(self, cell); if self.cursor.key.style == CursorStyle::Block { - mem::swap(&mut cell.bg, &mut cell.fg); - - if let Some(color) = self.cursor.text_color { - cell.fg = color; - } + cell.fg = self.cursor.text_color.color(cell.fg, cell.bg); } return Some(cell); @@ -475,24 +410,18 @@ impl<'a, C> Iterator for RenderableCellsIter<'a, C> { line: self.cursor.point.line, }; - let mut renderable_cell = - RenderableCell::new(self.config, self.colors, cell, selected); - - renderable_cell.inner = RenderableCellContent::Cursor(self.cursor.key); - - if let Some(color) = self.cursor.cursor_color { - renderable_cell.fg = color; - } + let mut cell = RenderableCell::new(self, cell); + cell.inner = RenderableCellContent::Cursor(self.cursor.key); + cell.fg = self.cursor.cursor_color.color(cell.fg, cell.bg); - return Some(renderable_cell); + return Some(cell); } } else { let cell = self.inner.next()?; + let cell = RenderableCell::new(self, cell); - let selected = self.is_selected(Point::new(cell.line, cell.column)); - - if !cell.is_empty() || selected { - return Some(RenderableCell::new(self.config, self.colors, cell, selected)); + if !cell.is_empty() { + return Some(cell); } } } @@ -802,6 +731,9 @@ pub struct Term<T> { /// Stack of saved window titles. When a title is popped from this stack, the `title` for the /// term is set, and the Glutin window's title attribute is changed through the event listener. title_stack: Vec<Option<String>>, + + /// Current forwards and backwards buffer search regexes. + regex_search: Option<RegexSearch>, } impl<T> Term<T> { @@ -810,8 +742,8 @@ impl<T> Term<T> { where T: EventListener, { - self.event_proxy.send_event(Event::MouseCursorDirty); self.grid.scroll_display(scroll); + self.event_proxy.send_event(Event::MouseCursorDirty); self.dirty = true; } @@ -823,9 +755,9 @@ impl<T> Term<T> { let grid = Grid::new(num_lines, num_cols, history_size, Cell::default()); let alt = Grid::new(num_lines, num_cols, 0 /* scroll history */, Cell::default()); - let tabs = TabStops::new(grid.num_cols()); + let tabs = TabStops::new(grid.cols()); - let scroll_region = Line(0)..grid.num_lines(); + let scroll_region = Line(0)..grid.screen_lines(); let colors = color::List::from(&config.colors); @@ -853,6 +785,7 @@ impl<T> Term<T> { default_title: config.window.title.clone(), title_stack: Vec::new(), selection: None, + regex_search: None, } } @@ -964,7 +897,7 @@ impl<T> Term<T> { tab_mode = true; } - if !cell.flags.contains(Flags::WIDE_CHAR_SPACER) { + if !cell.flags.intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER) { // Push cells primary character. text.push(cell.c); @@ -983,10 +916,9 @@ impl<T> Term<T> { } // If wide char is not part of the selection, but leading spacer is, include it. - if line_length == self.grid.num_cols() + if line_length == self.cols() && line_length.0 >= 2 - && grid_line[line_length - 1].flags.contains(Flags::WIDE_CHAR_SPACER) - && !grid_line[line_length - 2].flags.contains(Flags::WIDE_CHAR) + && grid_line[line_length - 1].flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) && include_wrapped_wide { text.push(self.grid[line - 1][Column(0)].c); @@ -1026,8 +958,8 @@ impl<T> Term<T> { /// Resize terminal to new dimensions. pub fn resize(&mut self, size: &SizeInfo) { - let old_cols = self.grid.num_cols(); - let old_lines = self.grid.num_lines(); + let old_cols = self.cols(); + let old_lines = self.screen_lines(); let num_cols = max(size.cols(), Column(MIN_SIZE)); let num_lines = max(size.lines(), Line(MIN_SIZE)); @@ -1038,6 +970,23 @@ impl<T> Term<T> { debug!("New num_cols is {} and num_lines is {}", num_cols, num_lines); + // Invalidate selection and tabs only when necessary. + if old_cols != num_cols { + self.selection = None; + + // Recreate tabs list. + self.tabs.resize(num_cols); + } else if let Some(selection) = self.selection.take() { + // Move the selection if only number of lines changed. + let delta = if num_lines > old_lines { + (num_lines - old_lines.0).saturating_sub(self.grid.history_size()) as isize + } else { + let cursor_line = self.grid.cursor.point.line; + -(min(old_lines - cursor_line - 1, old_lines - num_lines).0 as isize) + }; + self.selection = selection.rotate(self, &(Line(0)..num_lines), delta); + } + let is_alt = self.mode.contains(TermMode::ALT_SCREEN); self.grid.resize(!is_alt, num_lines, num_cols); @@ -1047,14 +996,11 @@ impl<T> Term<T> { self.vi_mode_cursor.point.col = min(self.vi_mode_cursor.point.col, num_cols - 1); self.vi_mode_cursor.point.line = min(self.vi_mode_cursor.point.line, num_lines - 1); - // Recreate tabs list. - self.tabs.resize(self.grid.num_cols()); - - // Reset scrolling region and selection. - self.scroll_region = Line(0)..self.grid.num_lines(); - self.selection = None; + // Reset scrolling region. + self.scroll_region = Line(0)..self.screen_lines(); } + /// Active terminal modes. #[inline] pub fn mode(&self) -> &TermMode { &self.mode @@ -1087,8 +1033,7 @@ impl<T> Term<T> { fn scroll_down_relative(&mut self, origin: Line, mut lines: Line) { trace!("Scrolling down relative: origin={}, lines={}", origin, lines); - let num_lines = self.grid.num_lines(); - let num_cols = self.grid.num_cols(); + let num_lines = self.screen_lines(); lines = min(lines, self.scroll_region.end - self.scroll_region.start); lines = min(lines, self.scroll_region.end - origin); @@ -1100,7 +1045,7 @@ impl<T> Term<T> { self.selection = self .selection .take() - .and_then(|s| s.rotate(num_lines, num_cols, &absolute_region, -(lines.0 as isize))); + .and_then(|s| s.rotate(self, &absolute_region, -(lines.0 as isize))); // Scroll between origin and bottom let template = Cell { bg: self.grid.cursor.template.bg, ..Cell::default() }; @@ -1114,8 +1059,8 @@ impl<T> Term<T> { #[inline] fn scroll_up_relative(&mut self, origin: Line, mut lines: Line) { trace!("Scrolling up relative: origin={}, lines={}", origin, lines); - let num_lines = self.grid.num_lines(); - let num_cols = self.grid.num_cols(); + + let num_lines = self.screen_lines(); lines = min(lines, self.scroll_region.end - self.scroll_region.start); @@ -1123,10 +1068,8 @@ impl<T> Term<T> { let absolute_region = (num_lines - region.end)..(num_lines - region.start); // Scroll selection. - self.selection = self - .selection - .take() - .and_then(|s| s.rotate(num_lines, num_cols, &absolute_region, lines.0 as isize)); + self.selection = + self.selection.take().and_then(|s| s.rotate(self, &absolute_region, lines.0 as isize)); // Scroll from origin to bottom less number of lines. let template = Cell { bg: self.grid.cursor.template.bg, ..Cell::default() }; @@ -1139,7 +1082,7 @@ impl<T> Term<T> { { // Setting 132 column font makes no sense, but run the other side effects. // Clear scrolling region. - self.set_scrolling_region(1, self.grid.num_lines().0); + self.set_scrolling_region(1, None); // Clear grid. let template = self.grid.cursor.template; @@ -1163,18 +1106,33 @@ impl<T> Term<T> { #[inline] pub fn toggle_vi_mode(&mut self) { self.mode ^= TermMode::VI; - self.selection = None; - // Reset vi mode cursor position to match primary cursor. - if self.mode.contains(TermMode::VI) { + let vi_mode = self.mode.contains(TermMode::VI); + + // Do not clear selection when entering search. + if self.regex_search.is_none() || !vi_mode { + self.selection = None; + } + + if vi_mode { + // Reset vi mode cursor position to match primary cursor. let cursor = self.grid.cursor.point; - let line = min(cursor.line + self.grid.display_offset(), self.lines() - 1); + let line = min(cursor.line + self.grid.display_offset(), self.screen_lines() - 1); self.vi_mode_cursor = ViModeCursor::new(Point::new(line, cursor.col)); + } else { + self.cancel_search(); } self.dirty = true; } + /// Start vi mode without moving the cursor. + #[inline] + pub fn set_vi_mode(&mut self) { + self.mode.insert(TermMode::VI); + self.dirty = true; + } + /// Move vi mode cursor. #[inline] pub fn vi_motion(&mut self, motion: ViMotion) @@ -1188,18 +1146,89 @@ impl<T> Term<T> { // Move cursor. self.vi_mode_cursor = self.vi_mode_cursor.motion(self, motion); + self.vi_mode_recompute_selection(); + + self.dirty = true; + } + + /// Move vi cursor to absolute point in grid. + #[inline] + pub fn vi_goto_point(&mut self, point: Point<usize>) + where + T: EventListener, + { + // Move viewport to make point visible. + self.scroll_to_point(point); + + // Move vi cursor to the point. + self.vi_mode_cursor.point = self.grid.clamp_buffer_to_visible(point); + + self.vi_mode_recompute_selection(); + + self.dirty = true; + } + + /// Update the active selection to match the vi mode cursor position. + #[inline] + fn vi_mode_recompute_selection(&mut self) { + // Require vi mode to be active. + if !self.mode.contains(TermMode::VI) { + return; + } - // Update selection if one is active. let viewport_point = self.visible_to_buffer(self.vi_mode_cursor.point); - if let Some(selection) = &mut self.selection { - // Do not extend empty selections started by a single mouse click. - if !selection.is_empty() { - selection.update(viewport_point, Side::Left); - selection.include_all(); - } + + // Update only if non-empty selection is present. + let selection = match &mut self.selection { + Some(selection) if !selection.is_empty() => selection, + _ => return, + }; + + selection.update(viewport_point, Side::Left); + selection.include_all(); + } + + /// Scroll display to point if it is outside of viewport. + pub fn scroll_to_point(&mut self, point: Point<usize>) + where + T: EventListener, + { + let display_offset = self.grid.display_offset(); + let num_lines = self.screen_lines().0; + + if point.line >= display_offset + num_lines { + let lines = point.line.saturating_sub(display_offset + num_lines - 1); + self.scroll_display(Scroll::Delta(lines as isize)); + } else if point.line < display_offset { + let lines = display_offset.saturating_sub(point.line); + self.scroll_display(Scroll::Delta(-(lines as isize))); } + } - self.dirty = true; + /// Jump to the end of a wide cell. + pub fn expand_wide(&self, mut point: Point<usize>, direction: Direction) -> Point<usize> { + let flags = self.grid[point.line][point.col].flags; + + match direction { + Direction::Right if flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) => { + point.col = Column(1); + point.line -= 1; + }, + Direction::Right if flags.contains(Flags::WIDE_CHAR) => point.col += 1, + Direction::Left if flags.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER) => { + if flags.contains(Flags::WIDE_CHAR_SPACER) { + point.col -= 1; + } + + let prev = point.sub_absolute(self, Boundary::Clamp, 1); + if self.grid[prev].flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) { + point = prev; + } + }, + _ => (), + } + + point } #[inline] @@ -1260,7 +1289,8 @@ impl<T> Term<T> { }; // Cursor shape. - let hidden = !self.mode.contains(TermMode::SHOW_CURSOR) || point.line >= self.lines(); + let hidden = + !self.mode.contains(TermMode::SHOW_CURSOR) || point.line >= self.screen_lines(); let cursor_style = if hidden && !vi_mode { point.line = Line(0); CursorStyle::Hidden @@ -1277,19 +1307,18 @@ impl<T> Term<T> { }; // Cursor colors. - let (text_color, cursor_color) = if vi_mode { - (config.vi_mode_cursor_text_color(), config.vi_mode_cursor_cursor_color()) + let color = if vi_mode { config.colors.vi_mode_cursor } else { config.colors.cursor }; + let cursor_color = if self.color_modified[NamedColor::Cursor as usize] { + CellRgb::Rgb(self.colors[NamedColor::Cursor]) } else { - let cursor_cursor_color = config.cursor_cursor_color().map(|c| self.colors[c]); - (config.cursor_text_color(), cursor_cursor_color) + color.cursor() }; + let text_color = color.text(); // Expand across wide cell when inside wide char or spacer. let buffer_point = self.visible_to_buffer(point); let cell = self.grid[buffer_point.line][buffer_point.col]; - let is_wide = if cell.flags.contains(Flags::WIDE_CHAR_SPACER) - && self.grid[buffer_point.line][buffer_point.col - 1].flags.contains(Flags::WIDE_CHAR) - { + let is_wide = if cell.flags.contains(Flags::WIDE_CHAR_SPACER) { point.col -= 1; true } else { @@ -1306,15 +1335,20 @@ impl<T> Term<T> { } } -impl<T> TermInfo for Term<T> { +impl<T> Dimensions for Term<T> { #[inline] - fn lines(&self) -> Line { - self.grid.num_lines() + fn cols(&self) -> Column { + self.grid.cols() } #[inline] - fn cols(&self) -> Column { - self.grid.num_cols() + fn screen_lines(&self) -> Line { + self.grid.screen_lines() + } + + #[inline] + fn total_lines(&self) -> usize { + self.grid.total_lines() } } @@ -1344,7 +1378,7 @@ impl<T: EventListener> Handler for Term<T> { self.wrapline(); } - let num_cols = self.grid.num_cols(); + let num_cols = self.cols(); // If in insert mode, first shift cells to the right. if self.mode.contains(TermMode::INSERT) && self.grid.cursor.point.col + width < num_cols { @@ -1365,7 +1399,7 @@ impl<T: EventListener> Handler for Term<T> { if self.grid.cursor.point.col + 1 >= num_cols { if self.mode.contains(TermMode::LINE_WRAP) { // Insert placeholder before wide char if glyph does not fit in this row. - self.write_at_cursor(' ').flags.insert(Flags::WIDE_CHAR_SPACER); + self.write_at_cursor(' ').flags.insert(Flags::LEADING_WIDE_CHAR_SPACER); self.wrapline(); } else { // Prevent out of bounds crash when linewrapping is disabled. @@ -1403,11 +1437,11 @@ impl<T: EventListener> Handler for Term<T> { let (y_offset, max_y) = if self.mode.contains(TermMode::ORIGIN) { (self.scroll_region.start, self.scroll_region.end - 1) } else { - (Line(0), self.grid.num_lines() - 1) + (Line(0), self.screen_lines() - 1) }; self.grid.cursor.point.line = min(line + y_offset, max_y); - self.grid.cursor.point.col = min(col, self.grid.num_cols() - 1); + self.grid.cursor.point.col = min(col, self.cols() - 1); self.grid.cursor.input_needs_wrap = false; } @@ -1428,11 +1462,11 @@ impl<T: EventListener> Handler for Term<T> { let cursor = self.grid.cursor; // Ensure inserting within terminal bounds - let count = min(count, self.grid.num_cols() - cursor.point.col); + let count = min(count, self.cols() - cursor.point.col); let source = cursor.point.col; let destination = cursor.point.col + count; - let num_cells = (self.grid.num_cols() - destination).0; + let num_cells = (self.cols() - destination).0; let line = &mut self.grid[cursor.point.line]; @@ -1467,7 +1501,7 @@ impl<T: EventListener> Handler for Term<T> { #[inline] fn move_forward(&mut self, cols: Column) { trace!("Moving forward: {}", cols); - let num_cols = self.grid.num_cols(); + let num_cols = self.cols(); self.grid.cursor.point.col = min(self.grid.cursor.point.col + cols, num_cols - 1); self.grid.cursor.input_needs_wrap = false; } @@ -1524,7 +1558,7 @@ impl<T: EventListener> Handler for Term<T> { return; } - while self.grid.cursor.point.col < self.grid.num_cols() && count != 0 { + while self.grid.cursor.point.col < self.cols() && count != 0 { count -= 1; let c = self.grid.cursor.charsets[self.active_charset].map('\t'); @@ -1534,7 +1568,7 @@ impl<T: EventListener> Handler for Term<T> { } loop { - if (self.grid.cursor.point.col + 1) == self.grid.num_cols() { + if (self.grid.cursor.point.col + 1) == self.cols() { break; } @@ -1573,7 +1607,7 @@ impl<T: EventListener> Handler for Term<T> { let next = self.grid.cursor.point.line + 1; if next == self.scroll_region.end { self.scroll_up(Line(1)); - } else if next < self.grid.num_lines() { + } else if next < self.screen_lines() { self.grid.cursor.point.line += 1; } } @@ -1653,7 +1687,7 @@ impl<T: EventListener> Handler for Term<T> { #[inline] fn delete_lines(&mut self, lines: Line) { let origin = self.grid.cursor.point.line; - let lines = min(self.lines() - origin, lines); + let lines = min(self.screen_lines() - origin, lines); trace!("Deleting {} lines", lines); @@ -1669,7 +1703,7 @@ impl<T: EventListener> Handler for Term<T> { trace!("Erasing chars: count={}, col={}", count, cursor.point.col); let start = cursor.point.col; - let end = min(start + count, self.grid.num_cols()); + let end = min(start + count, self.cols()); // Cleared cells have current background color set. let row = &mut self.grid[cursor.point.line]; @@ -1680,7 +1714,7 @@ impl<T: EventListener> Handler for Term<T> { #[inline] fn delete_chars(&mut self, count: Column) { - let cols = self.grid.num_cols(); + let cols = self.cols(); let cursor = self.grid.cursor; // Ensure deleting within terminal bounds. @@ -1768,7 +1802,7 @@ impl<T: EventListener> Handler for Term<T> { }, } - let cursor_buffer_line = (self.grid.num_lines() - self.grid.cursor.point.line - 1).0; + let cursor_buffer_line = (self.grid.screen_lines() - self.grid.cursor.point.line - 1).0; self.selection = self .selection .take() @@ -1850,7 +1884,7 @@ impl<T: EventListener> Handler for Term<T> { trace!("Clearing screen: {:?}", mode); let template = self.grid.cursor.template; - let num_lines = self.grid.num_lines().0; + let num_lines = self.screen_lines().0; let cursor_buffer_line = num_lines - self.grid.cursor.point.line.0 - 1; match mode { @@ -1864,7 +1898,7 @@ impl<T: EventListener> Handler for Term<T> { } // Clear up to the current column in the current line. - let end = min(cursor.col + 1, self.grid.num_cols()); + let end = min(cursor.col + 1, self.cols()); for cell in &mut self.grid[cursor.line][..end] { cell.reset(&template); } @@ -1933,17 +1967,18 @@ impl<T: EventListener> Handler for Term<T> { self.cursor_style = None; self.grid.reset(Cell::default()); self.inactive_grid.reset(Cell::default()); - self.scroll_region = Line(0)..self.grid.num_lines(); - self.tabs = TabStops::new(self.grid.num_cols()); + self.scroll_region = Line(0)..self.screen_lines(); + self.tabs = TabStops::new(self.cols()); self.title_stack = Vec::new(); self.title = None; self.selection = None; + self.regex_search = None; } #[inline] fn reverse_index(&mut self) { trace!("Reversing index"); - + // If cursor is at the top. if self.grid.cursor.point.line == self.scroll_region.start { self.scroll_down(Line(1)); } else { @@ -2074,7 +2109,10 @@ impl<T: EventListener> Handler for Term<T> { } #[inline] - fn set_scrolling_region(&mut self, top: usize, bottom: usize) { + fn set_scrolling_region(&mut self, top: usize, bottom: Option<usize>) { + // Fallback to the last line as default. + let bottom = bottom.unwrap_or_else(|| self.screen_lines().0); + if top >= bottom { debug!("Invalid scrolling region: ({};{})", top, bottom); return; @@ -2089,8 +2127,8 @@ impl<T: EventListener> Handler for Term<T> { trace!("Setting scrolling region: ({};{})", start, end); - self.scroll_region.start = min(start, self.grid.num_lines()); - self.scroll_region.end = min(end, self.grid.num_lines()); + self.scroll_region.start = min(start, self.screen_lines()); + self.scroll_region.end = min(end, self.screen_lines()); self.goto(Line(0), Column(0)); } @@ -2216,6 +2254,79 @@ impl IndexMut<Column> for TabStops { } } +/// Terminal test helpers. +pub mod test { + use super::*; + + use unicode_width::UnicodeWidthChar; + + use crate::config::Config; + use crate::index::Column; + + /// Construct a terminal from its content as string. + /// + /// A `\n` will break line and `\r\n` will break line without wrapping. + /// + /// # Examples + /// + /// ```rust + /// use alacritty_terminal::term::test::mock_term; + /// + /// // Create a terminal with the following cells: + /// // + /// // [h][e][l][l][o] <- WRAPLINE flag set + /// // [:][)][ ][ ][ ] + /// // [t][e][s][t][ ] + /// mock_term( + /// "\ + /// hello\n:)\r\ntest", + /// ); + /// ``` + pub fn mock_term(content: &str) -> Term<()> { + let lines: Vec<&str> = content.split('\n').collect(); + let num_cols = lines + .iter() + .map(|line| line.chars().filter(|c| *c != '\r').map(|c| c.width().unwrap()).sum()) + .max() + .unwrap_or(0); + + // Create terminal with the appropriate dimensions. + let size = SizeInfo { + width: num_cols as f32, + height: lines.len() as f32, + cell_width: 1., + cell_height: 1., + padding_x: 0., + padding_y: 0., + dpr: 1., + }; + let mut term = Term::new(&Config::<()>::default(), &size, ()); + + // Fill terminal with content. + for (line, text) in lines.iter().rev().enumerate() { + if !text.ends_with('\r') && line != 0 { + term.grid[line][Column(num_cols - 1)].flags.insert(Flags::WRAPLINE); + } + + let mut index = 0; + for c in text.chars().take_while(|c| *c != '\r') { + term.grid[line][Column(index)].c = c; + + // Handle fullwidth characters. + let width = c.width().unwrap(); + if width == 2 { + term.grid[line][Column(index)].flags.insert(Flags::WIDE_CHAR); + term.grid[line][Column(index + 1)].flags.insert(Flags::WIDE_CHAR_SPACER); + } + + index += width; + } + } + + term + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/alacritty_terminal/src/term/search.rs b/alacritty_terminal/src/term/search.rs new file mode 100644 index 00000000..b1766b05 --- /dev/null +++ b/alacritty_terminal/src/term/search.rs @@ -0,0 +1,794 @@ +use std::cmp::min; +use std::mem; +use std::ops::RangeInclusive; + +use regex_automata::{dense, DenseDFA, Error as RegexError, DFA}; + +use crate::grid::{BidirectionalIterator, Dimensions, GridIterator}; +use crate::index::{Boundary, Column, Direction, Point, Side}; +use crate::term::cell::{Cell, Flags}; +use crate::term::Term; + +/// Used to match equal brackets, when performing a bracket-pair selection. +const BRACKET_PAIRS: [(char, char); 4] = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')]; + +pub type Match = RangeInclusive<Point<usize>>; + +/// Terminal regex search state. +pub struct RegexSearch { + /// Locate end of match searching right. + right_fdfa: DenseDFA<Vec<usize>, usize>, + /// Locate start of match searching right. + right_rdfa: DenseDFA<Vec<usize>, usize>, + + /// Locate start of match searching left. + left_fdfa: DenseDFA<Vec<usize>, usize>, + /// Locate end of match searching left. + left_rdfa: DenseDFA<Vec<usize>, usize>, +} + +impl RegexSearch { + /// Build the forwards and backwards search DFAs. + pub fn new(search: &str) -> Result<RegexSearch, RegexError> { + // Check case info for smart case + let has_uppercase = search.chars().any(|c| c.is_uppercase()); + + // Create Regex DFAs for all search directions. + let mut builder = dense::Builder::new(); + let builder = builder.case_insensitive(!has_uppercase); + + let left_fdfa = builder.clone().reverse(true).build(search)?; + let left_rdfa = builder.clone().anchored(true).longest_match(true).build(search)?; + + let right_fdfa = builder.clone().build(search)?; + let right_rdfa = builder.anchored(true).longest_match(true).reverse(true).build(search)?; + + Ok(RegexSearch { right_fdfa, right_rdfa, left_fdfa, left_rdfa }) + } +} + +impl<T> Term<T> { + /// Enter terminal buffer search mode. + #[inline] + pub fn start_search(&mut self, search: &str) { + self.regex_search = RegexSearch::new(search).ok(); + self.dirty = true; + } + + /// Cancel active terminal buffer search. + #[inline] + pub fn cancel_search(&mut self) { + self.regex_search = None; + self.dirty = true; + } + + /// Get next search match in the specified direction. + pub fn search_next( + &self, + mut origin: Point<usize>, + direction: Direction, + side: Side, + mut max_lines: Option<usize>, + ) -> Option<Match> { + origin = self.expand_wide(origin, direction); + + max_lines = max_lines.filter(|max_lines| max_lines + 1 < self.total_lines()); + + match direction { + Direction::Right => self.next_match_right(origin, side, max_lines), + Direction::Left => self.next_match_left(origin, side, max_lines), + } + } + + /// Find the next match to the right of the origin. + fn next_match_right( + &self, + origin: Point<usize>, + side: Side, + max_lines: Option<usize>, + ) -> Option<Match> { + // Skip origin itself to exclude it from the search results. + let origin = origin.add_absolute(self, Boundary::Wrap, 1); + let start = self.line_search_left(origin); + let mut end = start; + + // Limit maximum number of lines searched. + let total_lines = self.total_lines(); + end = match max_lines { + Some(max_lines) => { + let line = (start.line + total_lines - max_lines) % total_lines; + Point::new(line, self.cols() - 1) + }, + _ => end.sub_absolute(self, Boundary::Wrap, 1), + }; + + let mut regex_iter = RegexIter::new(start, end, Direction::Right, &self).peekable(); + + // Check if there's any match at all. + let first_match = regex_iter.peek()?.clone(); + + let regex_match = regex_iter + .find(|regex_match| { + let match_point = Self::match_side(®ex_match, side); + + // If the match's point is beyond the origin, we're done. + match_point.line > start.line + || match_point.line < origin.line + || (match_point.line == origin.line && match_point.col >= origin.col) + }) + .unwrap_or(first_match); + + Some(regex_match) + } + + /// Find the next match to the left of the origin. + fn next_match_left( + &self, + origin: Point<usize>, + side: Side, + max_lines: Option<usize>, + ) -> Option<Match> { + // Skip origin itself to exclude it from the search results. + let origin = origin.sub_absolute(self, Boundary::Wrap, 1); + let start = self.line_search_right(origin); + let mut end = start; + + // Limit maximum number of lines searched. + end = match max_lines { + Some(max_lines) => Point::new((start.line + max_lines) % self.total_lines(), Column(0)), + _ => end.add_absolute(self, Boundary::Wrap, 1), + }; + + let mut regex_iter = RegexIter::new(start, end, Direction::Left, &self).peekable(); + + // Check if there's any match at all. + let first_match = regex_iter.peek()?.clone(); + + let regex_match = regex_iter + .find(|regex_match| { + let match_point = Self::match_side(®ex_match, side); + + // If the match's point is beyond the origin, we're done. + match_point.line < start.line + || match_point.line > origin.line + || (match_point.line == origin.line && match_point.col <= origin.col) + }) + .unwrap_or(first_match); + + Some(regex_match) + } + + /// Get the side of a match. + fn match_side(regex_match: &Match, side: Side) -> Point<usize> { + match side { + Side::Right => *regex_match.end(), + Side::Left => *regex_match.start(), + } + } + + /// Find the next regex match to the left of the origin point. + /// + /// The origin is always included in the regex. + pub fn regex_search_left(&self, start: Point<usize>, end: Point<usize>) -> Option<Match> { + let RegexSearch { left_fdfa: fdfa, left_rdfa: rdfa, .. } = self.regex_search.as_ref()?; + + // Find start and end of match. + let match_start = self.regex_search(start, end, Direction::Left, &fdfa)?; + let match_end = self.regex_search(match_start, start, Direction::Right, &rdfa)?; + + Some(match_start..=match_end) + } + + /// Find the next regex match to the right of the origin point. + /// + /// The origin is always included in the regex. + pub fn regex_search_right(&self, start: Point<usize>, end: Point<usize>) -> Option<Match> { + let RegexSearch { right_fdfa: fdfa, right_rdfa: rdfa, .. } = self.regex_search.as_ref()?; + + // Find start and end of match. + let match_end = self.regex_search(start, end, Direction::Right, &fdfa)?; + let match_start = self.regex_search(match_end, start, Direction::Left, &rdfa)?; + + Some(match_start..=match_end) + } + + /// Find the next regex match. + /// + /// This will always return the side of the first match which is farthest from the start point. + fn regex_search( + &self, + start: Point<usize>, + end: Point<usize>, + direction: Direction, + dfa: &impl DFA, + ) -> Option<Point<usize>> { + let last_line = self.total_lines() - 1; + let last_col = self.cols() - 1; + + // Advance the iterator. + let next = match direction { + Direction::Right => GridIterator::next, + Direction::Left => GridIterator::prev, + }; + + let mut iter = self.grid.iter_from(start); + let mut state = dfa.start_state(); + let mut regex_match = None; + + let mut cell = *iter.cell(); + self.skip_fullwidth(&mut iter, &mut cell, direction); + let mut point = iter.point(); + + loop { + // Convert char to array of bytes. + let mut buf = [0; 4]; + let utf8_len = cell.c.encode_utf8(&mut buf).len(); + + // Pass char to DFA as individual bytes. + for i in 0..utf8_len { + // Inverse byte order when going left. + let byte = match direction { + Direction::Right => buf[i], + Direction::Left => buf[utf8_len - i - 1], + }; + + // Since we get the state from the DFA, it doesn't need to be checked. + state = unsafe { dfa.next_state_unchecked(state, byte) }; + } + + // Handle regex state changes. + if dfa.is_match_or_dead_state(state) { + if dfa.is_dead_state(state) { + break; + } else { + regex_match = Some(point); + } + } + + // Stop once we've reached the target point. + if point == end { + break; + } + + // Advance grid cell iterator. + let mut new_cell = match next(&mut iter) { + Some(&cell) => cell, + None => { + // Wrap around to other end of the scrollback buffer. + let start = Point::new(last_line - point.line, last_col - point.col); + iter = self.grid.iter_from(start); + *iter.cell() + }, + }; + self.skip_fullwidth(&mut iter, &mut new_cell, direction); + let last_point = mem::replace(&mut point, iter.point()); + let last_cell = mem::replace(&mut cell, new_cell); + + // Handle linebreaks. + if (last_point.col == last_col + && point.col == Column(0) + && !last_cell.flags.contains(Flags::WRAPLINE)) + || (last_point.col == Column(0) + && point.col == last_col + && !cell.flags.contains(Flags::WRAPLINE)) + { + match regex_match { + Some(_) => break, + None => state = dfa.start_state(), + } + } + } + + regex_match + } + + /// Advance a grid iterator over fullwidth characters. + fn skip_fullwidth( + &self, + iter: &mut GridIterator<'_, Cell>, + cell: &mut Cell, + direction: Direction, + ) { + match direction { + Direction::Right if cell.flags.contains(Flags::WIDE_CHAR) => { + iter.next(); + }, + Direction::Right if cell.flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) => { + if let Some(new_cell) = iter.next() { + *cell = *new_cell; + } + iter.next(); + }, + Direction::Left if cell.flags.contains(Flags::WIDE_CHAR_SPACER) => { + if let Some(new_cell) = iter.prev() { + *cell = *new_cell; + } + + let prev = iter.point().sub_absolute(self, Boundary::Clamp, 1); + if self.grid[prev].flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) { + iter.prev(); + } + }, + _ => (), + } + } + + /// Find next matching bracket. + pub fn bracket_search(&self, point: Point<usize>) -> Option<Point<usize>> { + let start_char = self.grid[point.line][point.col].c; + + // Find the matching bracket we're looking for + let (forwards, end_char) = BRACKET_PAIRS.iter().find_map(|(open, close)| { + if open == &start_char { + Some((true, *close)) + } else if close == &start_char { + Some((false, *open)) + } else { + None + } + })?; + + let mut iter = self.grid.iter_from(point); + + // For every character match that equals the starting bracket, we + // ignore one bracket of the opposite type. + let mut skip_pairs = 0; + + loop { + // Check the next cell + let cell = if forwards { iter.next() } else { iter.prev() }; + + // Break if there are no more cells + let c = match cell { + Some(cell) => cell.c, + None => break, + }; + + // Check if the bracket matches + if c == end_char && skip_pairs == 0 { + return Some(iter.point()); + } else if c == start_char { + skip_pairs += 1; + } else if c == end_char { + skip_pairs -= 1; + } + } + + None + } + + /// Find left end of semantic block. + pub fn semantic_search_left(&self, mut point: Point<usize>) -> Point<usize> { + // Limit the starting point to the last line in the history + point.line = min(point.line, self.total_lines() - 1); + + let mut iter = self.grid.iter_from(point); + let last_col = self.cols() - Column(1); + + let wide = Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER; + while let Some(cell) = iter.prev() { + if !cell.flags.intersects(wide) && self.semantic_escape_chars.contains(cell.c) { + break; + } + + if iter.point().col == last_col && !cell.flags.contains(Flags::WRAPLINE) { + break; // cut off if on new line or hit escape char + } + + point = iter.point(); + } + + point + } + + /// Find right end of semantic block. + pub fn semantic_search_right(&self, mut point: Point<usize>) -> Point<usize> { + // Limit the starting point to the last line in the history + point.line = min(point.line, self.total_lines() - 1); + + let mut iter = self.grid.iter_from(point); + let last_col = self.cols() - 1; + + let wide = Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER; + while let Some(cell) = iter.next() { + if !cell.flags.intersects(wide) && self.semantic_escape_chars.contains(cell.c) { + break; + } + + point = iter.point(); + + if point.col == last_col && !cell.flags.contains(Flags::WRAPLINE) { + break; // cut off if on new line or hit escape char + } + } + + point + } + + /// Find the beginning of the current line across linewraps. + pub fn line_search_left(&self, mut point: Point<usize>) -> Point<usize> { + while point.line + 1 < self.total_lines() + && self.grid[point.line + 1][self.cols() - 1].flags.contains(Flags::WRAPLINE) + { + point.line += 1; + } + + point.col = Column(0); + + point + } + + /// Find the end of the current line across linewraps. + pub fn line_search_right(&self, mut point: Point<usize>) -> Point<usize> { + while self.grid[point.line][self.cols() - 1].flags.contains(Flags::WRAPLINE) { + point.line -= 1; + } + + point.col = self.cols() - 1; + + point + } +} + +/// Iterator over regex matches. +pub struct RegexIter<'a, T> { + point: Point<usize>, + end: Point<usize>, + direction: Direction, + term: &'a Term<T>, + done: bool, +} + +impl<'a, T> RegexIter<'a, T> { + pub fn new( + start: Point<usize>, + end: Point<usize>, + direction: Direction, + term: &'a Term<T>, + ) -> Self { + Self { point: start, done: false, end, direction, term } + } + + /// Skip one cell, advancing the origin point to the next one. + fn skip(&mut self) { + self.point = self.term.expand_wide(self.point, self.direction); + + self.point = match self.direction { + Direction::Right => self.point.add_absolute(self.term, Boundary::Wrap, 1), + Direction::Left => self.point.sub_absolute(self.term, Boundary::Wrap, 1), + }; + } + + /// Get the next match in the specified direction. + fn next_match(&self) -> Option<Match> { + match self.direction { + Direction::Right => self.term.regex_search_right(self.point, self.end), + Direction::Left => self.term.regex_search_left(self.point, self.end), + } + } +} + +impl<'a, T> Iterator for RegexIter<'a, T> { + type Item = Match; + + fn next(&mut self) -> Option<Self::Item> { + if self.point == self.end { + self.done = true; + } else if self.done { + return None; + } + + let regex_match = self.next_match()?; + + self.point = *regex_match.end(); + self.skip(); + + Some(regex_match) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::index::Column; + use crate::term::test::mock_term; + + #[test] + fn regex_right() { + #[rustfmt::skip] + let mut term = mock_term("\ + testing66\r\n\ + Alacritty\n\ + 123\r\n\ + Alacritty\r\n\ + 123\ + "); + + // Check regex across wrapped and unwrapped lines. + term.regex_search = Some(RegexSearch::new("Ala.*123").unwrap()); + let start = Point::new(3, Column(0)); + let end = Point::new(0, Column(2)); + let match_start = Point::new(3, Column(0)); + let match_end = Point::new(2, Column(2)); + assert_eq!(term.regex_search_right(start, end), Some(match_start..=match_end)); + } + + #[test] + fn regex_left() { + #[rustfmt::skip] + let mut term = mock_term("\ + testing66\r\n\ + Alacritty\n\ + 123\r\n\ + Alacritty\r\n\ + 123\ + "); + + // Check regex across wrapped and unwrapped lines. + term.regex_search = Some(RegexSearch::new("Ala.*123").unwrap()); + let start = Point::new(0, Column(2)); + let end = Point::new(3, Column(0)); + let match_start = Point::new(3, Column(0)); + let match_end = Point::new(2, Column(2)); + assert_eq!(term.regex_search_left(start, end), Some(match_start..=match_end)); + } + + #[test] + fn nested_regex() { + #[rustfmt::skip] + let mut term = mock_term("\ + Ala -> Alacritty -> critty\r\n\ + critty\ + "); + + // Greedy stopped at linebreak. + term.regex_search = Some(RegexSearch::new("Ala.*critty").unwrap()); + let start = Point::new(1, Column(0)); + let end = Point::new(1, Column(25)); + assert_eq!(term.regex_search_right(start, end), Some(start..=end)); + + // Greedy stopped at dead state. + term.regex_search = Some(RegexSearch::new("Ala[^y]*critty").unwrap()); + let start = Point::new(1, Column(0)); + let end = Point::new(1, Column(15)); + assert_eq!(term.regex_search_right(start, end), Some(start..=end)); + } + + #[test] + fn no_match_right() { + #[rustfmt::skip] + let mut term = mock_term("\ + first line\n\ + broken second\r\n\ + third\ + "); + + term.regex_search = Some(RegexSearch::new("nothing").unwrap()); + let start = Point::new(2, Column(0)); + let end = Point::new(0, Column(4)); + assert_eq!(term.regex_search_right(start, end), None); + } + + #[test] + fn no_match_left() { + #[rustfmt::skip] + let mut term = mock_term("\ + first line\n\ + broken second\r\n\ + third\ + "); + + term.regex_search = Some(RegexSearch::new("nothing").unwrap()); + let start = Point::new(0, Column(4)); + let end = Point::new(2, Column(0)); + assert_eq!(term.regex_search_left(start, end), None); + } + + #[test] + fn include_linebreak_left() { + #[rustfmt::skip] + let mut term = mock_term("\ + testing123\r\n\ + xxx\ + "); + + // Make sure the cell containing the linebreak is not skipped. + term.regex_search = Some(RegexSearch::new("te.*123").unwrap()); + let start = Point::new(0, Column(0)); + let end = Point::new(1, Column(0)); + let match_start = Point::new(1, Column(0)); + let match_end = Point::new(1, Column(9)); + assert_eq!(term.regex_search_left(start, end), Some(match_start..=match_end)); + } + + #[test] + fn include_linebreak_right() { + #[rustfmt::skip] + let mut term = mock_term("\ + xxx\r\n\ + testing123\ + "); + + // Make sure the cell containing the linebreak is not skipped. + term.regex_search = Some(RegexSearch::new("te.*123").unwrap()); + let start = Point::new(1, Column(2)); + let end = Point::new(0, Column(9)); + let match_start = Point::new(0, Column(0)); + assert_eq!(term.regex_search_right(start, end), Some(match_start..=end)); + } + + #[test] + fn skip_dead_cell() { + let mut term = mock_term("alacritty"); + + // Make sure dead state cell is skipped when reversing. + term.regex_search = Some(RegexSearch::new("alacrit").unwrap()); + let start = Point::new(0, Column(0)); + let end = Point::new(0, Column(6)); + assert_eq!(term.regex_search_right(start, end), Some(start..=end)); + } + + #[test] + fn reverse_search_dead_recovery() { + let mut term = mock_term("zooo lense"); + + // Make sure the reverse DFA operates the same as a forwards DFA. + term.regex_search = Some(RegexSearch::new("zoo").unwrap()); + let start = Point::new(0, Column(9)); + let end = Point::new(0, Column(0)); + let match_start = Point::new(0, Column(0)); + let match_end = Point::new(0, Column(2)); + assert_eq!(term.regex_search_left(start, end), Some(match_start..=match_end)); + } + + #[test] + fn multibyte_unicode() { + let mut term = mock_term("testвосибing"); + + term.regex_search = Some(RegexSearch::new("te.*ing").unwrap()); + let start = Point::new(0, Column(0)); + let end = Point::new(0, Column(11)); + assert_eq!(term.regex_search_right(start, end), Some(start..=end)); + + term.regex_search = Some(RegexSearch::new("te.*ing").unwrap()); + let start = Point::new(0, Column(11)); + let end = Point::new(0, Column(0)); + assert_eq!(term.regex_search_left(start, end), Some(end..=start)); + } + + #[test] + fn fullwidth() { + let mut term = mock_term("a🦇x🦇"); + + term.regex_search = Some(RegexSearch::new("[^ ]*").unwrap()); + let start = Point::new(0, Column(0)); + let end = Point::new(0, Column(5)); + assert_eq!(term.regex_search_right(start, end), Some(start..=end)); + + term.regex_search = Some(RegexSearch::new("[^ ]*").unwrap()); + let start = Point::new(0, Column(5)); + let end = Point::new(0, Column(0)); + assert_eq!(term.regex_search_left(start, end), Some(end..=start)); + } + + #[test] + fn singlecell_fullwidth() { + let mut term = mock_term("🦇"); + + term.regex_search = Some(RegexSearch::new("🦇").unwrap()); + let start = Point::new(0, Column(0)); + let end = Point::new(0, Column(1)); + assert_eq!(term.regex_search_right(start, end), Some(start..=end)); + + term.regex_search = Some(RegexSearch::new("🦇").unwrap()); + let start = Point::new(0, Column(1)); + let end = Point::new(0, Column(0)); + assert_eq!(term.regex_search_left(start, end), Some(end..=start)); + } + + #[test] + fn wrapping() { + #[rustfmt::skip] + let mut term = mock_term("\ + xxx\r\n\ + xxx\ + "); + + term.regex_search = Some(RegexSearch::new("xxx").unwrap()); + let start = Point::new(0, Column(2)); + let end = Point::new(1, Column(2)); + let match_start = Point::new(1, Column(0)); + assert_eq!(term.regex_search_right(start, end), Some(match_start..=end)); + + term.regex_search = Some(RegexSearch::new("xxx").unwrap()); + let start = Point::new(1, Column(0)); + let end = Point::new(0, Column(0)); + let match_end = Point::new(0, Column(2)); + assert_eq!(term.regex_search_left(start, end), Some(end..=match_end)); + } + + #[test] + fn wrapping_into_fullwidth() { + #[rustfmt::skip] + let mut term = mock_term("\ + 🦇xx\r\n\ + xx🦇\ + "); + + term.regex_search = Some(RegexSearch::new("🦇x").unwrap()); + let start = Point::new(0, Column(0)); + let end = Point::new(1, Column(3)); + let match_start = Point::new(1, Column(0)); + let match_end = Point::new(1, Column(2)); + assert_eq!(term.regex_search_right(start, end), Some(match_start..=match_end)); + + term.regex_search = Some(RegexSearch::new("x🦇").unwrap()); + let start = Point::new(1, Column(2)); + let end = Point::new(0, Column(0)); + let match_start = Point::new(0, Column(1)); + let match_end = Point::new(0, Column(3)); + assert_eq!(term.regex_search_left(start, end), Some(match_start..=match_end)); + } + + #[test] + fn leading_spacer() { + #[rustfmt::skip] + let mut term = mock_term("\ + xxx \n\ + 🦇xx\ + "); + term.grid[1][Column(3)].flags.insert(Flags::LEADING_WIDE_CHAR_SPACER); + + term.regex_search = Some(RegexSearch::new("🦇x").unwrap()); + let start = Point::new(1, Column(0)); + let end = Point::new(0, Column(3)); + let match_start = Point::new(1, Column(3)); + let match_end = Point::new(0, Column(2)); + assert_eq!(term.regex_search_right(start, end), Some(match_start..=match_end)); + + term.regex_search = Some(RegexSearch::new("🦇x").unwrap()); + let start = Point::new(0, Column(3)); + let end = Point::new(1, Column(0)); + let match_start = Point::new(1, Column(3)); + let match_end = Point::new(0, Column(2)); + assert_eq!(term.regex_search_left(start, end), Some(match_start..=match_end)); + + term.regex_search = Some(RegexSearch::new("x🦇").unwrap()); + let start = Point::new(1, Column(0)); + let end = Point::new(0, Column(3)); + let match_start = Point::new(1, Column(2)); + let match_end = Point::new(0, Column(1)); + assert_eq!(term.regex_search_right(start, end), Some(match_start..=match_end)); + + term.regex_search = Some(RegexSearch::new("x🦇").unwrap()); + let start = Point::new(0, Column(3)); + let end = Point::new(1, Column(0)); + let match_start = Point::new(1, Column(2)); + let match_end = Point::new(0, Column(1)); + assert_eq!(term.regex_search_left(start, end), Some(match_start..=match_end)); + } +} + +#[cfg(all(test, feature = "bench"))] +mod benches { + extern crate test; + + use super::*; + + use crate::term::test::mock_term; + + #[bench] + fn regex_search(b: &mut test::Bencher) { + let input = format!("{:^10000}", "Alacritty"); + let mut term = mock_term(&input); + term.regex_search = Some(RegexSearch::new(" Alacritty ").unwrap()); + let start = Point::new(0, Column(0)); + let end = Point::new(0, Column(input.len() - 1)); + + b.iter(|| { + test::black_box(term.regex_search_right(start, end)); + test::black_box(term.regex_search_left(end, start)); + }); + } +} diff --git a/alacritty_terminal/src/vi_mode.rs b/alacritty_terminal/src/vi_mode.rs index 6621eda5..985d5455 100644 --- a/alacritty_terminal/src/vi_mode.rs +++ b/alacritty_terminal/src/vi_mode.rs @@ -3,10 +3,10 @@ use std::cmp::{max, min}; use serde::Deserialize; use crate::event::EventListener; -use crate::grid::{GridCell, Scroll}; -use crate::index::{Column, Line, Point}; +use crate::grid::{Dimensions, GridCell}; +use crate::index::{Boundary, Column, Direction, Line, Point, Side}; use crate::term::cell::Flags; -use crate::term::{Search, Term}; +use crate::term::Term; /// Possible vi mode motion movements. #[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)] @@ -66,23 +66,23 @@ impl ViModeCursor { #[must_use = "this returns the result of the operation, without modifying the original"] pub fn motion<T: EventListener>(mut self, term: &mut Term<T>, motion: ViMotion) -> Self { let display_offset = term.grid().display_offset(); - let lines = term.grid().num_lines(); - let cols = term.grid().num_cols(); + let lines = term.grid().screen_lines(); + let cols = term.grid().cols(); let mut buffer_point = term.visible_to_buffer(self.point); match motion { ViMotion::Up => { - if buffer_point.line + 1 < term.grid().len() { + if buffer_point.line + 1 < term.grid().total_lines() { buffer_point.line += 1; } }, ViMotion::Down => buffer_point.line = buffer_point.line.saturating_sub(1), ViMotion::Left => { - buffer_point = expand_wide(term, buffer_point, true); + buffer_point = term.expand_wide(buffer_point, Direction::Left); let wrap_point = Point::new(buffer_point.line + 1, cols - 1); if buffer_point.col.0 == 0 - && buffer_point.line + 1 < term.grid().len() + && buffer_point.line + 1 < term.grid().total_lines() && is_wrap(term, wrap_point) { buffer_point = wrap_point; @@ -91,7 +91,7 @@ impl ViModeCursor { } }, ViMotion::Right => { - buffer_point = expand_wide(term, buffer_point, false); + buffer_point = term.expand_wide(buffer_point, Direction::Right); if is_wrap(term, buffer_point) { buffer_point = Point::new(buffer_point.line - 1, Column(0)); } else { @@ -99,9 +99,9 @@ impl ViModeCursor { } }, ViMotion::First => { - buffer_point = expand_wide(term, buffer_point, true); + buffer_point = term.expand_wide(buffer_point, Direction::Left); while buffer_point.col.0 == 0 - && buffer_point.line + 1 < term.grid().len() + && buffer_point.line + 1 < term.grid().total_lines() && is_wrap(term, Point::new(buffer_point.line + 1, cols - 1)) { buffer_point.line += 1; @@ -125,20 +125,36 @@ impl ViModeCursor { let col = first_occupied_in_line(term, line).unwrap_or_default().col; buffer_point = Point::new(line, col); }, - ViMotion::SemanticLeft => buffer_point = semantic(term, buffer_point, true, true), - ViMotion::SemanticRight => buffer_point = semantic(term, buffer_point, false, true), - ViMotion::SemanticLeftEnd => buffer_point = semantic(term, buffer_point, true, false), - ViMotion::SemanticRightEnd => buffer_point = semantic(term, buffer_point, false, false), - ViMotion::WordLeft => buffer_point = word(term, buffer_point, true, true), - ViMotion::WordRight => buffer_point = word(term, buffer_point, false, true), - ViMotion::WordLeftEnd => buffer_point = word(term, buffer_point, true, false), - ViMotion::WordRightEnd => buffer_point = word(term, buffer_point, false, false), + ViMotion::SemanticLeft => { + buffer_point = semantic(term, buffer_point, Direction::Left, Side::Left); + }, + ViMotion::SemanticRight => { + buffer_point = semantic(term, buffer_point, Direction::Right, Side::Left); + }, + ViMotion::SemanticLeftEnd => { + buffer_point = semantic(term, buffer_point, Direction::Left, Side::Right); + }, + ViMotion::SemanticRightEnd => { + buffer_point = semantic(term, buffer_point, Direction::Right, Side::Right); + }, + ViMotion::WordLeft => { + buffer_point = word(term, buffer_point, Direction::Left, Side::Left); + }, + ViMotion::WordRight => { + buffer_point = word(term, buffer_point, Direction::Right, Side::Left); + }, + ViMotion::WordLeftEnd => { + buffer_point = word(term, buffer_point, Direction::Left, Side::Right); + }, + ViMotion::WordRightEnd => { + buffer_point = word(term, buffer_point, Direction::Right, Side::Right); + }, ViMotion::Bracket => { buffer_point = term.bracket_search(buffer_point).unwrap_or(buffer_point); }, } - scroll_to_point(term, buffer_point); + term.scroll_to_point(buffer_point); self.point = term.grid().clamp_buffer_to_visible(buffer_point); self @@ -159,12 +175,12 @@ impl ViModeCursor { // Clamp movement to within visible region. let mut line = self.point.line.0 as isize; line -= overscroll; - line = max(0, min(term.grid().num_lines().0 as isize - 1, line)); + line = max(0, min(term.grid().screen_lines().0 as isize - 1, line)); // Find the first occupied cell after scrolling has been performed. let buffer_point = term.visible_to_buffer(self.point); let mut target_line = buffer_point.line as isize + lines; - target_line = max(0, min(term.grid().len() as isize - 1, target_line)); + target_line = max(0, min(term.grid().total_lines() as isize - 1, target_line)); let col = first_occupied_in_line(term, target_line as usize).unwrap_or_default().col; // Move cursor. @@ -174,27 +190,12 @@ impl ViModeCursor { } } -/// Scroll display if point is outside of viewport. -fn scroll_to_point<T: EventListener>(term: &mut Term<T>, point: Point<usize>) { - let display_offset = term.grid().display_offset(); - let lines = term.grid().num_lines(); - - // Scroll once the top/bottom has been reached. - if point.line >= display_offset + lines.0 { - let lines = point.line.saturating_sub(display_offset + lines.0 - 1); - term.scroll_display(Scroll::Lines(lines as isize)); - } else if point.line < display_offset { - let lines = display_offset.saturating_sub(point.line); - term.scroll_display(Scroll::Lines(-(lines as isize))); - }; -} - /// Find next end of line to move to. fn last<T>(term: &Term<T>, mut point: Point<usize>) -> Point<usize> { - let cols = term.grid().num_cols(); + let cols = term.grid().cols(); // Expand across wide cells. - point = expand_wide(term, point, false); + point = term.expand_wide(point, Direction::Right); // Find last non-empty cell in the current line. let occupied = last_occupied_in_line(term, point.line).unwrap_or_default(); @@ -217,10 +218,10 @@ fn last<T>(term: &Term<T>, mut point: Point<usize>) -> Point<usize> { /// Find next non-empty cell to move to. fn first_occupied<T>(term: &Term<T>, mut point: Point<usize>) -> Point<usize> { - let cols = term.grid().num_cols(); + let cols = term.grid().cols(); // Expand left across wide chars, since we're searching lines left to right. - point = expand_wide(term, point, true); + point = term.expand_wide(point, Direction::Left); // Find first non-empty cell in current line. let occupied = first_occupied_in_line(term, point.line) @@ -231,7 +232,7 @@ fn first_occupied<T>(term: &Term<T>, mut point: Point<usize>) -> Point<usize> { let mut occupied = None; // Search for non-empty cell in previous lines. - for line in (point.line + 1)..term.grid().len() { + for line in (point.line + 1)..term.grid().total_lines() { if !is_wrap(term, Point::new(line, cols - 1)) { break; } @@ -262,18 +263,18 @@ fn first_occupied<T>(term: &Term<T>, mut point: Point<usize>) -> Point<usize> { fn semantic<T: EventListener>( term: &mut Term<T>, mut point: Point<usize>, - left: bool, - start: bool, + direction: Direction, + side: Side, ) -> Point<usize> { // Expand semantically based on movement direction. let expand_semantic = |point: Point<usize>| { // Do not expand when currently on a semantic escape char. let cell = term.grid()[point.line][point.col]; if term.semantic_escape_chars().contains(cell.c) - && !cell.flags.contains(Flags::WIDE_CHAR_SPACER) + && !cell.flags.intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER) { point - } else if left { + } else if direction == Direction::Left { term.semantic_search_left(point) } else { term.semantic_search_right(point) @@ -281,27 +282,27 @@ fn semantic<T: EventListener>( }; // Make sure we jump above wide chars. - point = expand_wide(term, point, left); + point = term.expand_wide(point, direction); // Move to word boundary. - if left != start && !is_boundary(term, point, left) { + if direction != side && !is_boundary(term, point, direction) { point = expand_semantic(point); } // Skip whitespace. - let mut next_point = advance(term, point, left); - while !is_boundary(term, point, left) && is_space(term, next_point) { + let mut next_point = advance(term, point, direction); + while !is_boundary(term, point, direction) && is_space(term, next_point) { point = next_point; - next_point = advance(term, point, left); + next_point = advance(term, point, direction); } // Assure minimum movement of one cell. - if !is_boundary(term, point, left) { - point = advance(term, point, left); + if !is_boundary(term, point, direction) { + point = advance(term, point, direction); } // Move to word boundary. - if left == start && !is_boundary(term, point, left) { + if direction == side && !is_boundary(term, point, direction) { point = expand_semantic(point); } @@ -312,90 +313,71 @@ fn semantic<T: EventListener>( fn word<T: EventListener>( term: &mut Term<T>, mut point: Point<usize>, - left: bool, - start: bool, + direction: Direction, + side: Side, ) -> Point<usize> { // Make sure we jump above wide chars. - point = expand_wide(term, point, left); + point = term.expand_wide(point, direction); - if left == start { + if direction == side { // Skip whitespace until right before a word. - let mut next_point = advance(term, point, left); - while !is_boundary(term, point, left) && is_space(term, next_point) { + let mut next_point = advance(term, point, direction); + while !is_boundary(term, point, direction) && is_space(term, next_point) { point = next_point; - next_point = advance(term, point, left); + next_point = advance(term, point, direction); } // Skip non-whitespace until right inside word boundary. - let mut next_point = advance(term, point, left); - while !is_boundary(term, point, left) && !is_space(term, next_point) { + let mut next_point = advance(term, point, direction); + while !is_boundary(term, point, direction) && !is_space(term, next_point) { point = next_point; - next_point = advance(term, point, left); + next_point = advance(term, point, direction); } } - if left != start { + if direction != side { // Skip non-whitespace until just beyond word. - while !is_boundary(term, point, left) && !is_space(term, point) { - point = advance(term, point, left); + while !is_boundary(term, point, direction) && !is_space(term, point) { + point = advance(term, point, direction); } // Skip whitespace until right inside word boundary. - while !is_boundary(term, point, left) && is_space(term, point) { - point = advance(term, point, left); + while !is_boundary(term, point, direction) && is_space(term, point) { + point = advance(term, point, direction); } } point } -/// Jump to the end of a wide cell. -fn expand_wide<T, P>(term: &Term<T>, point: P, left: bool) -> Point<usize> -where - P: Into<Point<usize>>, -{ - let mut point = point.into(); - let cell = term.grid()[point.line][point.col]; - - if cell.flags.contains(Flags::WIDE_CHAR) && !left { - point.col += 1; - } else if cell.flags.contains(Flags::WIDE_CHAR_SPACER) - && term.grid()[point.line][point.col - 1].flags.contains(Flags::WIDE_CHAR) - && left - { - point.col -= 1; - } - - point -} - /// Find first non-empty cell in line. fn first_occupied_in_line<T>(term: &Term<T>, line: usize) -> Option<Point<usize>> { - (0..term.grid().num_cols().0) + (0..term.grid().cols().0) .map(|col| Point::new(line, Column(col))) .find(|&point| !is_space(term, point)) } /// Find last non-empty cell in line. fn last_occupied_in_line<T>(term: &Term<T>, line: usize) -> Option<Point<usize>> { - (0..term.grid().num_cols().0) + (0..term.grid().cols().0) .map(|col| Point::new(line, Column(col))) .rfind(|&point| !is_space(term, point)) } /// Advance point based on direction. -fn advance<T>(term: &Term<T>, point: Point<usize>, left: bool) -> Point<usize> { - if left { - point.sub_absolute(term.grid().num_cols(), 1) +fn advance<T>(term: &Term<T>, point: Point<usize>, direction: Direction) -> Point<usize> { + if direction == Direction::Left { + point.sub_absolute(term, Boundary::Clamp, 1) } else { - point.add_absolute(term.grid().num_cols(), 1) + point.add_absolute(term, Boundary::Clamp, 1) } } /// Check if cell at point contains whitespace. fn is_space<T>(term: &Term<T>, point: Point<usize>) -> bool { let cell = term.grid()[point.line][point.col]; - cell.c == ' ' || cell.c == '\t' && !cell.flags().contains(Flags::WIDE_CHAR_SPACER) + !cell.flags().intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER) + && (cell.c == ' ' || cell.c == '\t') } fn is_wrap<T>(term: &Term<T>, point: Point<usize>) -> bool { @@ -403,9 +385,11 @@ fn is_wrap<T>(term: &Term<T>, point: Point<usize>) -> bool { } /// Check if point is at screen boundary. -fn is_boundary<T>(term: &Term<T>, point: Point<usize>, left: bool) -> bool { - (point.line == 0 && point.col + 1 >= term.grid().num_cols() && !left) - || (point.line + 1 >= term.grid().len() && point.col.0 == 0 && left) +fn is_boundary<T>(term: &Term<T>, point: Point<usize>, direction: Direction) -> bool { + let total_lines = term.grid().total_lines(); + let num_cols = term.grid().cols(); + (point.line + 1 >= total_lines && point.col.0 == 0 && direction == Direction::Left) + || (point.line == 0 && point.col + 1 >= num_cols && direction == Direction::Right) } #[cfg(test)] diff --git a/alacritty_terminal/tests/ref.rs b/alacritty_terminal/tests/ref.rs index 9c5bbda5..62439775 100644 --- a/alacritty_terminal/tests/ref.rs +++ b/alacritty_terminal/tests/ref.rs @@ -8,11 +8,10 @@ use std::path::Path; use alacritty_terminal::ansi; use alacritty_terminal::config::MockConfig; use alacritty_terminal::event::{Event, EventListener}; +use alacritty_terminal::grid::{Dimensions, Grid}; use alacritty_terminal::index::Column; use alacritty_terminal::term::cell::Cell; -use alacritty_terminal::term::SizeInfo; -use alacritty_terminal::Grid; -use alacritty_terminal::Term; +use alacritty_terminal::term::{SizeInfo, Term}; macro_rules! ref_tests { ($($name:ident)*) => { @@ -114,8 +113,8 @@ fn ref_test(dir: &Path) { term_grid.truncate(); if grid != term_grid { - for i in 0..grid.len() { - for j in 0..grid.num_cols().0 { + for i in 0..grid.total_lines() { + for j in 0..grid.cols().0 { let cell = term_grid[i][Column(j)]; let original_cell = grid[i][Column(j)]; if original_cell != cell { |