feat: native Rust TTSR regex engine

feat: native Rust TTSR regex engine
This commit is contained in:
TÂCHES 2026-03-13 14:11:01 -06:00 committed by GitHub
commit 886bc9b571
10 changed files with 964 additions and 3 deletions

565
native/Cargo.lock generated
View file

@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "ahash"
version = "0.8.12"
@ -30,6 +36,26 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "arboard"
version = "3.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf"
dependencies = [
"clipboard-win",
"image",
"log",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation",
"parking_lot",
"percent-encoding",
"windows-sys 0.60.2",
"x11rb",
]
[[package]]
name = "ast-grep-core"
version = "0.39.9"
@ -51,12 +77,27 @@ dependencies = [
"memchr",
]
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "bit-set"
version = "0.8.0"
@ -89,6 +130,18 @@ dependencies = [
"serde",
]
[[package]]
name = "bytemuck"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "cc"
version = "1.2.56"
@ -105,6 +158,21 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clipboard-win"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
dependencies = [
"error-code",
]
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "const-random"
version = "0.1.18"
@ -134,6 +202,15 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
@ -175,6 +252,30 @@ dependencies = [
"syn",
]
[[package]]
name = "dashmap"
version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
dependencies = [
"cfg-if",
"crossbeam-utils",
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]]
name = "dispatch2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [
"bitflags",
"objc2",
]
[[package]]
name = "either"
version = "1.15.0"
@ -205,24 +306,106 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "error-code"
version = "3.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
[[package]]
name = "fancy-regex"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f"
dependencies = [
"bit-set",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fax"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab"
dependencies = [
"fax_derive",
]
[[package]]
name = "fax_derive"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "fdeflate"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
dependencies = [
"simd-adler32",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "flate2"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "gethostname"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
dependencies = [
"rustix",
"windows-link",
]
[[package]]
name = "getrandom"
version = "0.2.17"
@ -234,6 +417,16 @@ dependencies = [
"wasi",
]
[[package]]
name = "gif"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e"
dependencies = [
"color_quant",
"weezl",
]
[[package]]
name = "globset"
version = "0.4.18"
@ -338,11 +531,23 @@ dependencies = [
name = "gsd-engine"
version = "0.1.0"
dependencies = [
"arboard",
"dashmap",
"globset",
"gsd-ast",
"gsd-grep",
"html-to-markdown-rs",
"ignore",
"image",
"libc",
"napi",
"napi-build",
"napi-derive",
"regex",
"smallvec",
"syntect",
"unicode-segmentation",
"unicode-width",
]
[[package]]
@ -356,6 +561,23 @@ dependencies = [
"rayon",
]
[[package]]
name = "half"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hashbrown"
version = "0.16.1"
@ -419,6 +641,35 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "image"
version = "0.25.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [
"bytemuck",
"byteorder-lite",
"color_quant",
"gif",
"image-webp",
"moxcms",
"num-traits",
"png",
"tiff",
"zune-core",
"zune-jpeg",
]
[[package]]
name = "image-webp"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
dependencies = [
"byteorder-lite",
"quick-error",
]
[[package]]
name = "indexmap"
version = "2.13.0"
@ -426,7 +677,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown",
"hashbrown 0.16.1",
]
[[package]]
@ -451,6 +702,12 @@ dependencies = [
"windows-link",
]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "lock_api"
version = "0.4.14"
@ -472,7 +729,7 @@ version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
dependencies = [
"hashbrown",
"hashbrown 0.16.1",
]
[[package]]
@ -501,6 +758,26 @@ dependencies = [
"libc",
]
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
name = "moxcms"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
dependencies = [
"num-traits",
"pxfm",
]
[[package]]
name = "napi"
version = "2.16.17"
@ -564,6 +841,88 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "objc2"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
dependencies = [
"objc2-encode",
]
[[package]]
name = "objc2-app-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
dependencies = [
"bitflags",
"objc2",
"objc2-core-graphics",
"objc2-foundation",
]
[[package]]
name = "objc2-core-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags",
"dispatch2",
"objc2",
]
[[package]]
name = "objc2-core-graphics"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
dependencies = [
"bitflags",
"dispatch2",
"objc2",
"objc2-core-foundation",
"objc2-io-surface",
]
[[package]]
name = "objc2-encode"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
[[package]]
name = "objc2-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags",
"objc2",
"objc2-core-foundation",
]
[[package]]
name = "objc2-io-surface"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
dependencies = [
"bitflags",
"objc2",
"objc2-core-foundation",
]
[[package]]
name = "once_cell"
version = "1.21.4"
@ -593,6 +952,12 @@ dependencies = [
"windows-link",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "phf"
version = "0.13.1"
@ -646,6 +1011,19 @@ dependencies = [
"siphasher",
]
[[package]]
name = "png"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
dependencies = [
"bitflags",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]]
name = "precomputed-hash"
version = "0.1.1"
@ -661,6 +1039,18 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "pxfm"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d"
[[package]]
name = "quick-error"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quote"
version = "1.0.45"
@ -728,6 +1118,19 @@ version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "same-file"
version = "1.0.6"
@ -798,6 +1201,12 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "simd-adler32"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]]
name = "siphasher"
version = "1.0.2"
@ -851,6 +1260,24 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "syntect"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925"
dependencies = [
"bincode",
"fancy-regex",
"flate2",
"fnv",
"once_cell",
"regex-syntax",
"serde",
"serde_derive",
"thiserror",
"walkdir",
]
[[package]]
name = "tendril"
version = "0.5.0"
@ -881,6 +1308,20 @@ dependencies = [
"syn",
]
[[package]]
name = "tiff"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
dependencies = [
"fax",
"flate2",
"half",
"quick-error",
"weezl",
"zune-jpeg",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
@ -1292,6 +1733,12 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "utf-8"
version = "0.7.6"
@ -1338,13 +1785,19 @@ dependencies = [
"string_cache_codegen",
]
[[package]]
name = "weezl"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@ -1353,6 +1806,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
@ -1362,6 +1824,88 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "x11rb"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
dependencies = [
"gethostname",
"rustix",
"x11rb-protocol",
]
[[package]]
name = "x11rb-protocol"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
[[package]]
name = "zerocopy"
version = "0.8.42"
@ -1387,3 +1931,18 @@ name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "zune-core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
[[package]]
name = "zune-jpeg"
version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c"
dependencies = [
"zune-core",
]

View file

@ -26,6 +26,7 @@ image = { version = "0.25", default-features = false, features = [
] }
napi = { version = "2", features = ["napi8"] }
napi-derive = "2"
regex = "1"
smallvec = "1"
syntect = { version = "5", default-features = false, features = ["default-syntaxes", "default-themes", "regex-fancy"] }
unicode-segmentation = "1"

View file

@ -19,4 +19,5 @@ mod html;
mod ps;
mod task;
mod text;
mod ttsr;
mod image;

View file

@ -0,0 +1,124 @@
//! N-API bindings for the TTSR (Time Traveling Stream Rules) regex engine.
//!
//! Pre-compiles all rule condition patterns into a `regex::RegexSet` so that
//! `checkBuffer` can test all patterns against the accumulated stream buffer
//! in a single DFA pass, instead of iterating O(rules x conditions) in JS.
use napi::bindgen_prelude::*;
use napi_derive::napi;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
/// Maps a compiled regex back to its owning rule.
struct PatternMapping {
/// Index into the RegexSet's pattern list.
_pattern_index: usize,
/// Name of the rule this pattern belongs to.
rule_name: String,
}
struct CompiledRuleSet {
regex_set: regex::RegexSet,
mappings: Vec<PatternMapping>,
}
// Global handle store — handles are u64 keys into this map.
static NEXT_HANDLE: AtomicU64 = AtomicU64::new(1);
static STORE: std::sync::LazyLock<Mutex<HashMap<u64, CompiledRuleSet>>> =
std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
#[napi(object)]
pub struct NapiTtsrRuleInput {
pub name: String,
pub conditions: Vec<String>,
}
/// Compile a set of TTSR rules into an optimized regex engine.
///
/// Returns an opaque numeric handle. Each rule has one or more regex condition
/// patterns. All patterns are compiled into a single `RegexSet` for O(1)-style
/// matching against the stream buffer.
#[napi(js_name = "ttsrCompileRules")]
pub fn ttsr_compile_rules(rules: Vec<NapiTtsrRuleInput>) -> Result<f64> {
let mut patterns: Vec<String> = Vec::new();
let mut mappings: Vec<PatternMapping> = Vec::new();
for rule in &rules {
for condition in &rule.conditions {
let idx = patterns.len();
patterns.push(condition.clone());
mappings.push(PatternMapping {
_pattern_index: idx,
rule_name: rule.name.clone(),
});
}
}
if patterns.is_empty() {
return Err(Error::from_reason("No valid patterns provided"));
}
let regex_set = regex::RegexSet::new(&patterns)
.map_err(|e| Error::from_reason(format!("Failed to compile regex set: {e}")))?;
let handle = NEXT_HANDLE.fetch_add(1, Ordering::Relaxed);
let compiled = CompiledRuleSet {
regex_set,
mappings,
};
STORE
.lock()
.map_err(|e| Error::from_reason(format!("Lock poisoned: {e}")))?
.insert(handle, compiled);
// Return as f64 since napi BigInt interop is awkward; handles won't exceed 2^53.
Ok(handle as f64)
}
/// Check a buffer against compiled TTSR rules.
///
/// Returns an array of unique rule names whose conditions matched the buffer.
/// The RegexSet tests all patterns in a single pass over the buffer.
#[napi(js_name = "ttsrCheckBuffer")]
pub fn ttsr_check_buffer(handle: f64, buffer: String) -> Result<Vec<String>> {
let handle_key = handle as u64;
let store = STORE
.lock()
.map_err(|e| Error::from_reason(format!("Lock poisoned: {e}")))?;
let compiled = store
.get(&handle_key)
.ok_or_else(|| Error::from_reason(format!("Invalid TTSR handle: {handle}")))?;
let matching_indices: Vec<usize> = compiled.regex_set.matches(&buffer).into_iter().collect();
// Deduplicate: multiple conditions from the same rule should produce one entry.
let mut seen = std::collections::HashSet::new();
let mut matched_rules: Vec<String> = Vec::new();
for idx in matching_indices {
let rule_name = &compiled.mappings[idx].rule_name;
if seen.insert(rule_name.clone()) {
matched_rules.push(rule_name.clone());
}
}
Ok(matched_rules)
}
/// Free a compiled TTSR rule set, releasing memory.
#[napi(js_name = "ttsrFreeRules")]
pub fn ttsr_free_rules(handle: f64) -> Result<()> {
let handle_key = handle as u64;
STORE
.lock()
.map_err(|e| Error::from_reason(format!("Lock poisoned: {e}")))?
.remove(&handle_key);
Ok(())
}

View file

@ -0,0 +1,135 @@
import { test, describe } from "node:test";
import assert from "node:assert/strict";
import { createRequire } from "node:module";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
// Load the native addon directly
const addonDir = path.resolve(__dirname, "..", "..", "..", "..", "native", "addon");
const platformTag = `${process.platform}-${process.arch}`;
const candidates = [
path.join(addonDir, `gsd_engine.${platformTag}.node`),
path.join(addonDir, "gsd_engine.dev.node"),
];
let native;
for (const candidate of candidates) {
try {
native = require(candidate);
break;
} catch {
// try next
}
}
if (!native) {
console.error("Native addon not found. Run `npm run build:native -w @gsd/native` first.");
process.exit(1);
}
describe("native ttsr: ttsrCompileRules()", () => {
test("compiles rules and returns a numeric handle", () => {
const handle = native.ttsrCompileRules([
{ name: "rule1", conditions: ["foo", "bar"] },
]);
assert.equal(typeof handle, "number");
assert.ok(handle > 0);
native.ttsrFreeRules(handle);
});
test("rejects empty conditions", () => {
assert.throws(() => {
native.ttsrCompileRules([]);
});
});
test("rejects invalid regex patterns", () => {
assert.throws(() => {
native.ttsrCompileRules([
{ name: "bad", conditions: ["(unclosed"] },
]);
});
});
});
describe("native ttsr: ttsrCheckBuffer()", () => {
test("returns matching rule names", () => {
const handle = native.ttsrCompileRules([
{ name: "greet", conditions: ["hello\\s+world"] },
{ name: "farewell", conditions: ["goodbye"] },
]);
const matches = native.ttsrCheckBuffer(handle, "say hello world please");
assert.deepEqual(matches, ["greet"]);
native.ttsrFreeRules(handle);
});
test("returns multiple matching rules", () => {
const handle = native.ttsrCompileRules([
{ name: "a", conditions: ["alpha"] },
{ name: "b", conditions: ["beta"] },
{ name: "c", conditions: ["gamma"] },
]);
const matches = native.ttsrCheckBuffer(handle, "alpha and beta together");
assert.ok(matches.includes("a"));
assert.ok(matches.includes("b"));
assert.ok(!matches.includes("c"));
native.ttsrFreeRules(handle);
});
test("returns empty array on no match", () => {
const handle = native.ttsrCompileRules([
{ name: "x", conditions: ["zzz_no_match"] },
]);
const matches = native.ttsrCheckBuffer(handle, "nothing here");
assert.deepEqual(matches, []);
native.ttsrFreeRules(handle);
});
test("deduplicates when multiple conditions of same rule match", () => {
const handle = native.ttsrCompileRules([
{ name: "multi", conditions: ["foo", "bar"] },
]);
const matches = native.ttsrCheckBuffer(handle, "foo and bar");
assert.deepEqual(matches, ["multi"]);
native.ttsrFreeRules(handle);
});
test("handles large buffers efficiently", () => {
const handle = native.ttsrCompileRules([
{ name: "needle", conditions: ["NEEDLE_PATTERN_XYZ"] },
]);
// 1MB buffer with the needle near the end
const bigBuffer = "x".repeat(1024 * 1024) + "NEEDLE_PATTERN_XYZ";
const matches = native.ttsrCheckBuffer(handle, bigBuffer);
assert.deepEqual(matches, ["needle"]);
native.ttsrFreeRules(handle);
});
});
describe("native ttsr: ttsrFreeRules()", () => {
test("frees handle without error", () => {
const handle = native.ttsrCompileRules([
{ name: "temp", conditions: ["tmp"] },
]);
native.ttsrFreeRules(handle);
});
test("rejects invalid handle on check", () => {
assert.throws(() => {
native.ttsrCheckBuffer(99999, "test");
});
});
});

View file

@ -83,3 +83,6 @@ export type {
export { parseImage, ImageFormat, SamplingFilter } from "./image/index.js";
export type { NativeImageHandle } from "./image/index.js";
export { ttsrCompileRules, ttsrCheckBuffer, ttsrFreeRules } from "./ttsr/index.js";
export type { TtsrHandle, TtsrRuleInput } from "./ttsr/index.js";

View file

@ -88,4 +88,7 @@ export const native = loadNative() as {
visibleWidth: (text: string, tabWidth?: number) => number;
fuzzyFind: (options: unknown) => unknown;
NativeImage: unknown;
ttsrCompileRules: (rules: unknown[]) => number;
ttsrCheckBuffer: (handle: number, buffer: string) => string[];
ttsrFreeRules: (handle: number) => void;
};

View file

@ -0,0 +1,39 @@
/**
* Native TTSR regex engine.
*
* Pre-compiles all rule condition patterns into a single Rust RegexSet for
* O(1)-style matching per buffer check, replacing per-rule JS regex iteration.
*/
import { native } from "../native.js";
import type { TtsrHandle, TtsrRuleInput } from "./types.js";
export type { TtsrHandle, TtsrRuleInput };
/**
* Compile TTSR rules into an optimized native regex engine.
*
* Returns an opaque handle for use with `ttsrCheckBuffer` and `ttsrFreeRules`.
*/
export function ttsrCompileRules(rules: TtsrRuleInput[]): TtsrHandle {
return native.ttsrCompileRules(rules) as TtsrHandle;
}
/**
* Check a buffer against compiled TTSR rules.
*
* Returns an array of unique rule names whose conditions matched.
* All patterns are tested in a single pass via Rust's RegexSet.
*/
export function ttsrCheckBuffer(handle: TtsrHandle, buffer: string): string[] {
return native.ttsrCheckBuffer(handle, buffer) as string[];
}
/**
* Free a compiled TTSR rule set, releasing native memory.
*
* Call when rules are no longer needed (e.g., session end).
*/
export function ttsrFreeRules(handle: TtsrHandle): void {
native.ttsrFreeRules(handle);
}

View file

@ -0,0 +1,10 @@
/** Input rule for TTSR regex compilation. */
export interface TtsrRuleInput {
/** Unique rule name. */
name: string;
/** Regex condition patterns (any match triggers the rule). */
conditions: string[];
}
/** Opaque handle to a compiled TTSR rule set. */
export type TtsrHandle = number;

View file

@ -4,9 +4,34 @@
* Manages rules that get injected mid-stream when their condition pattern matches
* the agent's output. When a match occurs, the stream is aborted, the rule is
* injected as a system reminder, and the request is retried.
*
* The regex hot-path is delegated to a native Rust RegexSet engine when
* available, testing all patterns in a single DFA pass. Falls back to
* per-rule JS RegExp iteration when the native module is not loaded.
*/
import picomatch from "picomatch";
// ── Native TTSR engine (optional) ─────────────────────────────────────
let nativeTtsr: {
ttsrCompileRules: (rules: { name: string; conditions: string[] }[]) => number;
ttsrCheckBuffer: (handle: number, buffer: string) => string[];
ttsrFreeRules: (handle: number) => void;
} | null = null;
try {
// Dynamic import to avoid hard dependency — gracefully degrades to JS.
const native = await import("@gsd/native");
if (native.ttsrCompileRules && native.ttsrCheckBuffer && native.ttsrFreeRules) {
nativeTtsr = {
ttsrCompileRules: native.ttsrCompileRules,
ttsrCheckBuffer: native.ttsrCheckBuffer,
ttsrFreeRules: native.ttsrFreeRules,
};
}
} catch {
// Native module not available — JS fallback will be used.
}
export type TtsrMatchSource = "text" | "thinking" | "tool";
/** Context about the stream content currently being checked against TTSR rules. */
@ -86,6 +111,8 @@ export class TtsrManager {
readonly #injectionRecords = new Map<string, InjectionRecord>();
readonly #buffers = new Map<string, string>();
#messageCount = 0;
#nativeHandle: number | null = null;
#nativeDirty = false;
constructor(settings?: TtsrSettings) {
this.#settings = { ...DEFAULT_SETTINGS, ...settings };
@ -245,6 +272,40 @@ export class TtsrManager {
return false;
}
/** Compile (or recompile) the native RegexSet from all current rules. */
#compileNative(): void {
if (!nativeTtsr || !this.#nativeDirty) return;
// Free previous handle if any.
if (this.#nativeHandle !== null) {
try {
nativeTtsr.ttsrFreeRules(this.#nativeHandle);
} catch { /* ignore */ }
this.#nativeHandle = null;
}
const ruleInputs: { name: string; conditions: string[] }[] = [];
for (const [, entry] of this.#rules) {
ruleInputs.push({
name: entry.rule.name,
conditions: entry.rule.condition,
});
}
if (ruleInputs.length === 0) {
this.#nativeDirty = false;
return;
}
try {
this.#nativeHandle = nativeTtsr.ttsrCompileRules(ruleInputs);
} catch (err) {
console.warn(`[ttsr] Native compilation failed, using JS fallback: ${(err as Error).message}`);
this.#nativeHandle = null;
}
this.#nativeDirty = false;
}
/** Add a TTSR rule to be monitored. */
addRule(rule: Rule): boolean {
if (this.#rules.has(rule.name)) return false;
@ -257,6 +318,7 @@ export class TtsrManager {
const globalPathMatchers = this.#compileGlobalPathMatchers(rule.globs);
this.#rules.set(rule.name, { rule, conditions, scope, globalPathMatchers });
this.#nativeDirty = true;
return true;
}
@ -265,6 +327,10 @@ export class TtsrManager {
*
* Buffers are isolated by source/tool key so matches don't bleed across
* assistant prose, thinking text, and unrelated tool argument streams.
*
* When the native Rust engine is available, all regex conditions are tested
* in a single DFA pass via RegexSet. Scope, glob, and repeat-gate checks
* remain in JS as they are lightweight and context-dependent.
*/
checkDelta(delta: string, context: TtsrMatchContext): Rule[] {
const bufferKey = this.#bufferKey(context);
@ -275,6 +341,26 @@ export class TtsrManager {
}
this.#buffers.set(bufferKey, nextBuffer);
// Lazily compile native engine if rules changed.
if (this.#nativeDirty) this.#compileNative();
// ── Native path: single-pass RegexSet match ───────────────────────
if (nativeTtsr && this.#nativeHandle !== null) {
const regexMatchedNames = nativeTtsr.ttsrCheckBuffer(this.#nativeHandle, nextBuffer);
const regexMatchedSet = new Set(regexMatchedNames);
const matches: Rule[] = [];
for (const [name, entry] of this.#rules) {
if (!regexMatchedSet.has(name)) continue;
if (!this.#canTrigger(name)) continue;
if (!this.#matchesScope(entry, context)) continue;
if (!this.#matchesGlobalPaths(entry, context)) continue;
matches.push(entry.rule);
}
return matches;
}
// ── JS fallback: per-rule regex iteration ─────────────────────────
const matches: Rule[] = [];
for (const [name, entry] of this.#rules) {
if (!this.#canTrigger(name)) continue;