diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000000..a301bb9070b8 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "steel"] + path = steel + url = https://github.com/mattwparas/steel.git diff --git a/Cargo.lock b/Cargo.lock index cabb98a70b0e..050f796b9d38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,19 +3,61 @@ version = 3 [[package]] -name = "addr2line" -version = "0.22.0" +name = "abi_stable" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d6512d3eb05ffe5004c59c206de7f99c34951504056ce23fc953842f12c445" +dependencies = [ + "abi_stable_derive", + "abi_stable_shared", + "const_panic", + "core_extensions", + "crossbeam-channel", + "generational-arena", + "libloading 0.7.4", + "lock_api", + "parking_lot", + "paste", + "repr_offset", + "rustc_version", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "abi_stable_derive" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "d7178468b407a4ee10e881bc7a328a65e739f0863615cca4429d43916b05e898" dependencies = [ - "gimli", + "abi_stable_shared", + "as_derive_utils", + "core_extensions", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", + "typed-arena", ] [[package]] -name = "adler" -version = "1.0.2" +name = "abi_stable_shared" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b5df7688c123e63f4d4d649cba63f2967ba7f7861b1664fca3f77d3dad2b63" +dependencies = [ + "core_extensions", +] + +[[package]] +name = "addr2line" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] [[package]] name = "adler2" @@ -33,7 +75,7 @@ dependencies = [ "getrandom 0.2.15", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -47,9 +89,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-tzdata" @@ -78,25 +120,74 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "as_derive_utils" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff3c96645900a44cf11941c111bd08a6573b0e2f9f69bc9264b179d8fae753c4" +dependencies = [ + "core_extensions", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "async-ffi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4de21c0feef7e5a556e51af767c953f0501f7f300ba785cc99c47bdc8081a50" +dependencies = [ + "abi_stable", +] + [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", - "miniz_oxide 0.7.4", + "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "bigdecimal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f31f3af01c5c65a07985c804d3366560e6fa7883d640a122819b14ec327482c" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", ] [[package]] @@ -105,11 +196,20 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + [[package]] name = "bstr" -version = "1.10.0" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" dependencies = [ "memchr", "regex-automata", @@ -118,15 +218,21 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "byteorder" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cassowary" @@ -134,6 +240,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.17" @@ -187,6 +302,55 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59" +[[package]] +name = "codegen" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff61280aed771c3070e7dcc9e050c66f1eb1e3b96431ba66f9f74641d02fc41d" +dependencies = [ + "indexmap 1.9.3", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width 0.1.12", +] + +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "serde", + "static_assertions", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const_panic" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2459fc9262a1aa204eb4b5764ad4f189caec88aea9634389c0a25f8be7f6265e" + [[package]] name = "content_inspector" version = "0.2.4" @@ -196,12 +360,36 @@ dependencies = [ "memchr", ] +[[package]] +name = "coolor" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "691defa50318376447a73ced869862baecfab35f6aabaa91a4cd726b315bfe1a" +dependencies = [ + "crossterm", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core_extensions" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92c71dc07c9721607e7a16108336048ee978c3a8b129294534272e8bac96c0ee" +dependencies = [ + "core_extensions_proc_macros", +] + +[[package]] +name = "core_extensions_proc_macros" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f3b219d28b6e3b4ac87bc1fc522e0803ab22e055da177bff0068c4150c61a6" + [[package]] name = "crc32fast" version = "1.4.2" @@ -211,11 +399,59 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crokey" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520e83558f4c008ac06fa6a86e5c1d4357be6f994cce7434463ebcdaadf47bb1" +dependencies = [ + "crokey-proc_macros", + "crossterm", + "once_cell", + "serde", + "strict", +] + +[[package]] +name = "crokey-proc_macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "370956e708a1ce65fe4ac5bb7185791e0ece7485087f17736d54a23a0895049f" +dependencies = [ + "crossterm", + "proc-macro2", + "quote", + "strict", + "syn 1.0.109", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -230,11 +466,20 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crossterm" @@ -278,6 +523,27 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -286,7 +552,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -297,9 +563,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encoding_rs" @@ -327,9 +593,9 @@ checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" @@ -343,9 +609,9 @@ dependencies = [ [[package]] name = "error-code" -version = "3.2.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" +checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" [[package]] name = "etcetera" @@ -369,9 +635,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fern" @@ -384,9 +650,9 @@ dependencies = [ [[package]] name = "filedescriptor" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" dependencies = [ "libc", "thiserror 1.0.69", @@ -407,12 +673,12 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.33" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" dependencies = [ "crc32fast", - "miniz_oxide 0.8.0", + "miniz_oxide", ] [[package]] @@ -453,6 +719,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "futures-task" version = "0.3.31" @@ -466,12 +743,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", + "futures-macro", "futures-task", "pin-project-lite", "pin-utils", "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generational-arena" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877e94aff08e743b651baaea359664321055749b398adff8740a7399af7796e7" +dependencies = [ + "cfg-if", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -485,21 +781,23 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ "cfg-if", + "js-sys", "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "gix" @@ -562,7 +860,7 @@ dependencies = [ "gix-utils", "itoa", "thiserror 2.0.12", - "winnow 0.6.18", + "winnow 0.6.26", ] [[package]] @@ -644,7 +942,7 @@ dependencies = [ "smallvec", "thiserror 2.0.12", "unicode-bom", - "winnow 0.6.18", + "winnow 0.6.26", ] [[package]] @@ -886,7 +1184,7 @@ dependencies = [ "itoa", "smallvec", "thiserror 2.0.12", - "winnow 0.6.18", + "winnow 0.6.26", ] [[package]] @@ -996,7 +1294,7 @@ dependencies = [ "gix-utils", "maybe-async", "thiserror 2.0.12", - "winnow 0.6.18", + "winnow 0.6.26", ] [[package]] @@ -1028,7 +1326,7 @@ dependencies = [ "gix-validate", "memmap2", "thiserror 2.0.12", - "winnow 0.6.18", + "winnow 0.6.26", ] [[package]] @@ -1294,6 +1592,12 @@ dependencies = [ "memmap2", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -1302,6 +1606,7 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", + "serde", ] [[package]] @@ -1345,6 +1650,7 @@ dependencies = [ "slotmap", "smallvec", "smartstring", + "steel-core", "textwrap", "toml", "tree-sitter", @@ -1391,7 +1697,7 @@ dependencies = [ "cc", "etcetera", "helix-stdx", - "libloading", + "libloading 0.8.6", "log", "once_cell", "serde", @@ -1479,7 +1785,7 @@ dependencies = [ "helix-vcs", "helix-view", "ignore", - "indexmap", + "indexmap 2.8.0", "indoc", "libc", "log", @@ -1493,6 +1799,8 @@ dependencies = [ "signal-hook", "signal-hook-tokio", "smallvec", + "steel-core", + "steel-doc", "tempfile", "termini", "thiserror 2.0.12", @@ -1513,6 +1821,7 @@ dependencies = [ "helix-view", "log", "once_cell", + "steel-core", "termini", "unicode-segmentation", ] @@ -1560,6 +1869,7 @@ dependencies = [ "serde", "serde_json", "slotmap", + "steel-core", "tempfile", "thiserror 2.0.12", "tokio", @@ -1574,25 +1884,49 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "home" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", ] +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core", ] @@ -1721,7 +2055,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -1761,6 +2095,43 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "im" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +dependencies = [ + "bitmaps", + "rand_core 0.6.4", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] + +[[package]] +name = "im-lists" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88485149c4fcec01ebce4e4b8284a3c75b3d8a4749169f5481144e6433e9bcd2" +dependencies = [ + "smallvec", +] + +[[package]] +name = "im-rc" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe" +dependencies = [ + "bitmaps", + "rand_core 0.6.4", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] + [[package]] name = "imara-diff" version = "0.1.8" @@ -1770,6 +2141,16 @@ dependencies = [ "hashbrown 0.15.2", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.8.0" @@ -1807,41 +2188,46 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.1.13" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a45489186a6123c128fdf6016183fcfab7113e1820eb813127e036e287233fb" +checksum = "c04ef77ae73f3cf50510712722f0c4e8b46f5aaa1bf5ffad2ae213e6495e78e5" dependencies = [ "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", "windows-sys 0.59.0", ] [[package]] name = "jiff-tzdb" -version = "0.1.1" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91335e575850c5c4c673b9bd467b0e025f164ca59d0564f69d0c2ee0ffad4653" +checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" [[package]] name = "jiff-tzdb-platform" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9835f0060a626fe59f160437bc725491a6af23133ea906500027d1bd2f8f4329" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" dependencies = [ "jiff-tzdb", ] [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -1854,12 +2240,57 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "lasso" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e14eda50a3494b3bf7b9ce51c52434a761e383d7238ce1dd5dcec2fbc13e9fb" +dependencies = [ + "ahash", + "dashmap", + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "lazy-regex" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60c7310b93682b36b98fa7ea4de998d3463ccbebd94d935d6b48ba5b6ffa7126" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba01db5ef81e17eb10a5e0f2109d1b3a3e29bac3070fdbd7d156bf7dbd206a1" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.100", +] + [[package]] name = "libc" version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "libloading" version = "0.8.6" @@ -1867,9 +2298,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets", + "windows-targets 0.52.6", ] +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + [[package]] name = "libredox" version = "0.1.3" @@ -1883,21 +2320,21 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" +checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" [[package]] name = "litemap" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" [[package]] name = "lock_api" @@ -1923,7 +2360,7 @@ checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -1934,38 +2371,37 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memmap2" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" dependencies = [ "libc", ] [[package]] -name = "miniz_oxide" -version = "0.7.4" +name = "minimad" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +checksum = "a9c5d708226d186590a7b6d4a9780e2bdda5f689e0d58cd17012a298efd745d2" dependencies = [ - "adler", + "once_cell", ] [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi", "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", @@ -1993,6 +2429,73 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2008,15 +2511,15 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", ] [[package]] name = "object" -version = "0.36.4" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -2038,6 +2541,12 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "parking_lot" version = "0.12.3" @@ -2058,14 +2567,20 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathdiff" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "percent-encoding" @@ -2075,9 +2590,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -2085,31 +2600,84 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "polling" +version = "3.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix 0.38.44", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "portable-atomic" -version = "1.7.0" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy 0.8.24", +] + +[[package]] +name = "pretty" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" +checksum = "ac98773b7109bc75f475ab5a134c9b64b87e59d776d31098d8f346922396a477" +dependencies = [ + "arrayvec", + "typed-arena", + "unicode-width 0.1.12", +] [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] [[package]] name = "prodash" -version = "29.0.0" +version = "29.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a266d8d6020c61a437be704c5e618037588e1985c7dbb7bf8d265db84cffe325" +checksum = "9ee7ce24c980b976607e2d6ae4aae92827994d23fed71659c3ede3f92528b58b" dependencies = [ "log", "parking_lot", ] +[[package]] +name = "psm" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58e5423e24c18cc840e1c98370b3993c6649cd1678b4d24318bcf0a083cbe88" +dependencies = [ + "cc", +] + [[package]] name = "pulldown-cmark" version = "0.13.0" @@ -2127,25 +2695,68 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ - "rand", + "rand 0.8.5", +] + +[[package]] +name = "quickscope" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d47bcfc3e13850589cf9338a02b6dfb5aebb3748a0f93a392e8df91d6193b6b" +dependencies = [ + "indexmap 1.9.3", + "smallvec", ] [[package]] name = "quote" -version = "1.0.37" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "radix_fmt" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce082a9940a7ace2ad4a8b7d0b1eac6aa378895f18be598230c5f2284ac05426" + [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha", + "rand_core 0.9.3", + "zerocopy 0.8.24", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -2157,6 +2768,24 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "rayon" version = "1.10.0" @@ -2179,13 +2808,24 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.3" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "regex" version = "1.11.1" @@ -2228,6 +2868,15 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "repr_offset" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1070755bd29dffc19d0971cab794e607839ba2ef4b69a9e6fbc8733c1b72ea" +dependencies = [ + "tstr", +] + [[package]] name = "ropey" version = "1.6.1" @@ -2244,6 +2893,15 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.44" @@ -2253,7 +2911,7 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.4.14", + "linux-raw-sys 0.4.15", "windows-sys 0.59.0", ] @@ -2266,15 +2924,21 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.9.2", + "linux-raw-sys 0.9.3", "windows-sys 0.59.0", ] +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -2291,6 +2955,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + [[package]] name = "serde" version = "1.0.219" @@ -2308,7 +2978,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -2392,6 +3062,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + [[package]] name = "slab" version = "0.4.9" @@ -2435,9 +3115,9 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", @@ -2449,23 +3129,154 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "stacker" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601f9201feb9b09c00266478bf459952b9ef9a6b94edb2f21eba14ab681a60a9" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] + [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "steel-core" +version = "0.6.0" +source = "git+https://github.com/mattwparas/steel.git#a93900c4f44cd2b9bc065b63c867a8973620f113" +dependencies = [ + "abi_stable", + "anyhow", + "arc-swap", + "async-ffi", + "bigdecimal", + "bincode", + "chrono", + "codespan-reporting", + "compact_str", + "crossbeam", + "dirs", + "futures-executor", + "futures-task", + "futures-util", + "fxhash", + "getrandom 0.3.2", + "home", + "http", + "httparse", + "im", + "im-lists", + "im-rc", + "lasso", + "log", + "num", + "once_cell", + "parking_lot", + "polling", + "pretty", + "quickscope", + "radix_fmt", + "rand 0.9.0", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "stacker", + "steel-derive", + "steel-gen", + "steel-parser", + "strsim", + "termimad", + "weak-table", + "which", +] + +[[package]] +name = "steel-derive" +version = "0.5.0" +source = "git+https://github.com/mattwparas/steel.git#a93900c4f44cd2b9bc065b63c867a8973620f113" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "steel-doc" +version = "0.6.0" +source = "git+https://github.com/mattwparas/steel.git#a93900c4f44cd2b9bc065b63c867a8973620f113" +dependencies = [ + "steel-core", +] + +[[package]] +name = "steel-gen" +version = "0.2.0" +source = "git+https://github.com/mattwparas/steel.git#a93900c4f44cd2b9bc065b63c867a8973620f113" +dependencies = [ + "codegen", + "serde", + "serde_derive", +] + +[[package]] +name = "steel-parser" +version = "0.6.0" +source = "git+https://github.com/mattwparas/steel.git#a93900c4f44cd2b9bc065b63c867a8973620f113" +dependencies = [ + "compact_str", + "fxhash", + "lasso", + "num", + "once_cell", + "pretty", + "serde", + "serde_derive", + "smallvec", +] + [[package]] name = "str_indices" -version = "0.4.3" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6" + +[[package]] +name = "strict" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f42444fea5b87a39db4218d9422087e66a85d0e7a0963a439b07bcdf91804006" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9557cb6521e8d009c51a8666f09356f4b817ba9ba0981a305bd86aee47bd35c" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] [[package]] name = "syn" -version = "2.0.87" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", @@ -2480,7 +3291,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -2490,12 +3301,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ "fastrand", - "getrandom 0.3.1", + "getrandom 0.3.2", "once_cell", "rustix 1.0.3", "windows-sys 0.59.0", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "termimad" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8e19c6dbf107bec01d0e216bb8219485795b7d75328e4fa5ef2756c1be4f8dc" +dependencies = [ + "coolor", + "crokey", + "crossbeam", + "lazy-regex", + "minimad", + "serde", + "thiserror 1.0.69", + "unicode-width 0.1.12", +] + [[package]] name = "termini" version = "1.0.0" @@ -2542,7 +3378,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -2553,7 +3389,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -2577,9 +3413,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" dependencies = [ "tinyvec_macros", ] @@ -2616,7 +3452,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -2657,13 +3493,29 @@ version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ - "indexmap", + "indexmap 2.8.0", "serde", "serde_spanned", "toml_datetime", - "winnow 0.7.2", + "winnow 0.7.4", ] +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" + [[package]] name = "tree-sitter" version = "0.22.6" @@ -2675,14 +3527,38 @@ dependencies = [ ] [[package]] -name = "unicase" -version = "2.7.0" +name = "tstr" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +checksum = "7f8e0294f14baae476d0dd0a2d780b2e24d66e349a9de876f5126777a37bdba7" dependencies = [ - "version_check", + "tstr_proc_macros", ] +[[package]] +name = "tstr_proc_macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78122066b0cb818b8afd08f7ed22f7fdbc3e90815035726f0840d0d26c0747a" + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-bom" version = "2.0.3" @@ -2697,9 +3573,9 @@ checksum = "24adfe8311434967077a6adff125729161e6e4934d76f6b7c55318ac5c9246d3" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-linebreak" @@ -2709,9 +3585,9 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] @@ -2782,44 +3658,44 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" -version = "0.13.3+wasi-0.2.2" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.100", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2827,22 +3703,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "weak-table" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "323f4da9523e9a669e1eaf9c6e763892769b1d38c623913647bfdc1532fe4549" [[package]] name = "which" @@ -2893,14 +3778,23 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] name = "windows-link" -version = "0.1.0" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-sys" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] [[package]] name = "windows-sys" @@ -2908,7 +3802,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -2917,7 +3811,22 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -2926,28 +3835,46 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2960,24 +3887,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -2986,18 +3937,18 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" dependencies = [ "memchr", ] [[package]] name = "winnow" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603" +checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" dependencies = [ "memchr", ] @@ -3010,9 +3961,9 @@ checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] name = "wit-bindgen-rt" -version = "0.33.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags", ] @@ -3042,9 +3993,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", @@ -3054,13 +4005,13 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", "synstructure", ] @@ -3070,7 +4021,16 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive 0.8.24", ] [[package]] @@ -3081,27 +4041,38 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", ] [[package]] name = "zerofrom" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", "synstructure", ] @@ -3124,5 +4095,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] diff --git a/Cargo.toml b/Cargo.toml index 667a83967726..2538ddaa5bbc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,9 @@ package.helix-term.opt-level = 2 tree-sitter = { version = "0.22" } nucleo = "0.5.0" slotmap = "1.0.7" +# If working locally, use the local path dependency +# steel-core = { path = "/Users/matt/code/steel/crates/steel-core", version = "0.6.0", features = ["anyhow", "dylibs", "sync"] } +steel-core = { git = "https://github.com/mattwparas/steel.git", version = "0.6.0", features = ["anyhow", "dylibs", "sync"] } thiserror = "2.0" tempfile = "3.19.1" bitflags = "2.9" @@ -56,4 +59,5 @@ categories = ["editor"] repository = "https://github.com/helix-editor/helix" homepage = "https://helix-editor.com" license = "MPL-2.0" -rust-version = "1.76" +# TODO: Get this back to 1.76 +rust-version = "1.81" diff --git a/STEEL.md b/STEEL.md new file mode 100644 index 000000000000..1120f3a0eeba --- /dev/null +++ b/STEEL.md @@ -0,0 +1,166 @@ +# Building + +You will need: + +* A clone of this fork, on the branch `steel-event-system` + +`steel` is included as a git submodule for ease of building. + +## Installing helix + +Just run + +`cargo xtask steel` + +To install the `hx` executable, with steel as a plugin language. This also includes: + +The `steel` executable, the steel language server, the steel dylib installer, and the steel standard library. + + +## Setting up configurations for helix + +Note, this API is entirely subjet to change, and I promise absolutely 0 backwards compatibility while this is in development. + +There are 2 important files you'll want, which should be auto generated during the installation process: + +* `~/.config/helix/helix.scm` +* `~/.config/helix/init.scm` + +Note - these both live inside the same directory that helix sets up for runtime configurations. + + +### `helix.scm` + +The `helix.scm` module will be loaded first before anything else, the runtime will `require` this module, and any functions exported will now be available +to be used as typed commands. For example: + + +```scheme +# helix.scm +(require "helix/editor.scm") +(require (prefix-in helix. "helix/commands.scm")) +(require (prefix-in helix.static. "helix/static.scm")) + +(provide shell git-add open-helix-scm open-init-scm) + +;;@doc +;; Specialized shell implementation, where % is a wildcard for the current file +(define (shell cx . args) + ;; Replace the % with the current file + (define expanded (map (lambda (x) (if (equal? x "%") (current-path cx) x)) args)) + (apply helix.run-shell-command expanded)) + +;;@doc +;; Adds the current file to git +(define (git-add cx) + (shell cx "git" "add" "%")) + +(define (current-path) + (let* ([focus (editor-focus)] + [focus-doc-id (editor->doc-id focus)]) + (editor-document->path focus-doc-id))) + +;;@doc +;; Open the helix.scm file +(define (open-helix-scm) + (helix.open (helix.static.get-helix-scm-path))) + +;;@doc +;; Opens the init.scm file +(define (open-init-scm) + (helix.open (helix.static.get-init-scm-path))) + + +``` + +Now, if you'd like to add the current file you're editing to git, simply type `:git-add` - you'll see the doc pop up with it since we've annotated the function +with the `@doc` symbol. Hitting enter will execute the command. + +You can also conveniently open the `helix.scm` file by using the typed command `:open-helix-scm`. + + +### `init.scm` + +The `init.scm` file is run at the top level, immediately after the `helix.scm` module is `require`d. The helix context is available here, so you can interact with the editor. + +The helix context is bound to the top level variable `*helix.cx*`. + +For example, if we wanted to select a random theme at startup: + +```scheme +# init.scm + +(require-builtin steel/random as rand::) +(require (prefix-in helix. "helix/commands.scm")) +(require (prefix-in helix.static. "helix/static.scm")) + +(define rng (rand::thread-rng!)) + +;; Picking one from the possible themes +(define possible-themes '("ayu_mirage" "tokyonight_storm" "catppuccin_macchiato")) + +(define (select-random lst) + (let ([index (rand::rng->gen-range rng 0 (length lst))]) (list-ref lst index))) + +(define (randomly-pick-theme options) + ;; Randomly select the theme from the possible themes list + (helix.theme (select-random options))) + +(randomly-pick-theme possible-themes) + +``` + +### Libraries for helix + +There are a handful of extra libraries in development for extending helix, and can be found here https://github.com/mattwparas/helix-config. + +If you'd like to use them, create a directory called `cogs` in your `.config/helix` directory, and copy the files in there. + +### options.scm + +If you'd like to override configurations from your toml config: + + +```scheme +# init.scm + +(require "helix/configuration.scm") + +(file-picker (fp-hidden #f)) +(cursorline #t) +(soft-wrap (sw-enable #t)) + +``` + + +### keymaps.scm + +Applying custom keybindings for certain file extensions: + + +```scheme +# init.scm + +(require "cogs/keymaps.scm") +(require (only-in "cogs/file-tree.scm" FILE-TREE-KEYBINDINGS FILE-TREE)) +(require (only-in "cogs/recentf.scm" recentf-open-files get-recent-files recentf-snapshot)) + +;; Set the global keybinding for now +(add-global-keybinding (hash "normal" (hash "C-r" (hash "f" ":recentf-open-files")))) + +(define scm-keybindings (hash "insert" (hash "ret" ':scheme-indent "C-l" ':insert-lambda))) + +;; Grab whatever the existing keybinding map is +(define standard-keybindings (deep-copy-global-keybindings)) + +(define file-tree-base (deep-copy-global-keybindings)) + +(merge-keybindings standard-keybindings scm-keybindings) +(merge-keybindings file-tree-base FILE-TREE-KEYBINDINGS) + +(set-global-buffer-or-extension-keymap (hash "scm" standard-keybindings FILE-TREE file-tree-base)) + +``` + +In insert mode, this overrides the `ret` keybinding to instead use a custom scheme indent function. Functions _must_ be available as typed commands, and are referred to +as symbols. So in this case, the `scheme-indent` function was exported by my `helix.scm` module. diff --git a/flake.nix b/flake.nix index a334a345db09..cd5b20f0629c 100644 --- a/flake.nix +++ b/flake.nix @@ -8,79 +8,204 @@ url = "github:oxalica/rust-overlay"; inputs.nixpkgs.follows = "nixpkgs"; }; + crane.url = "github:ipetkov/crane"; }; outputs = { self, nixpkgs, + crane, flake-utils, rust-overlay, ... - }: let - gitRev = self.rev or self.dirtyRev or null; - in + }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; overlays = [(import rust-overlay)]; }; + mkRootPath = rel: + builtins.path { + path = "${toString ./.}/${rel}"; + name = rel; + }; + filteredSource = let + pathsToIgnore = [ + ".envrc" + ".ignore" + ".github" + ".gitignore" + "logo_dark.svg" + "logo_light.svg" + "rust-toolchain.toml" + "rustfmt.toml" + "runtime" + "screenshot.png" + "book" + "docs" + "README.md" + "CHANGELOG.md" + "shell.nix" + "default.nix" + "grammars.nix" + "flake.nix" + "flake.lock" + ]; + ignorePaths = path: type: let + inherit (nixpkgs) lib; + # split the nix store path into its components + components = lib.splitString "/" path; + # drop off the `/nix/hash-source` section from the path + relPathComponents = lib.drop 4 components; + # reassemble the path components + relPath = lib.concatStringsSep "/" relPathComponents; + in + lib.all (p: ! (lib.hasPrefix p relPath)) pathsToIgnore; + in + builtins.path { + name = "helix-source"; + path = toString ./.; + # filter out unnecessary paths + filter = ignorePaths; + }; + + helix-cogs = craneLibStable.buildPackage (commonArgs // { + pname = "helix-cogs"; + version = "0.1.0"; + cargoArtifacts = craneLibStable.buildDepsOnly commonArgs; + + buildPhase = '' + export HOME=$PWD/build_home # code-gen will write files relative to $HOME + cargoBuildLog=$(mktemp cargoBuildLogXXXX.json) + cargo run --package xtask -- code-gen --message-format json-render-diagnostics >"$cargoBuildLog" + ''; - # Get Helix's MSRV toolchain to build with by default. - msrvToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; - msrvPlatform = pkgs.makeRustPlatform { - cargo = msrvToolchain; - rustc = msrvToolchain; + postInstall = '' + mkdir -p $out/cogs + cp -r build_home/.config/helix/* "$out/cogs" + ''; + + }); + + makeOverridableHelix = old: config: let + grammars = pkgs.callPackage ./grammars.nix config; + runtimeDir = pkgs.runCommand "helix-runtime" {} '' + mkdir -p $out + ln -s ${mkRootPath "runtime"}/* $out + rm -r $out/grammars + ln -s ${grammars} $out/grammars + ''; + helix-wrapped = + pkgs.runCommand + old.name + { + inherit (old) pname version; + meta = old.meta or {}; + passthru = + (old.passthru or {}) + // { + unwrapped = old; + }; + nativeBuildInputs = [pkgs.makeWrapper]; + makeWrapperArgs = config.makeWrapperArgs or []; + } + '' + cp -rs --no-preserve=mode,ownership ${old} $out + wrapProgram "$out/bin/hx" ''${makeWrapperArgs[@]} --set HELIX_RUNTIME "${runtimeDir}" + ''; + in + helix-wrapped + // { + override = makeOverridableHelix old; + passthru = + helix-wrapped.passthru + // { + wrapper = old: makeOverridableHelix old config; + }; + }; + stdenv = + if pkgs.stdenv.isLinux + then pkgs.stdenv + else pkgs.clangStdenv; + rustFlagsEnv = pkgs.lib.optionalString stdenv.isLinux "-C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment --cfg tokio_unstable"; + rustToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; + craneLibMSRV = (crane.mkLib pkgs).overrideToolchain rustToolchain; + craneLibStable = (crane.mkLib pkgs).overrideToolchain pkgs.pkgsBuildHost.rust-bin.stable.latest.default; + commonArgs = { + inherit stdenv; + inherit (craneLibMSRV.crateNameFromCargoToml {cargoToml = ./helix-term/Cargo.toml;}) pname; + inherit (craneLibMSRV.crateNameFromCargoToml {cargoToml = ./Cargo.toml;}) version; + src = filteredSource; + # disable fetching and building of tree-sitter grammars in the helix-term build.rs + HELIX_DISABLE_AUTO_GRAMMAR_BUILD = "1"; + buildInputs = [stdenv.cc.cc.lib]; + nativeBuildInputs = [pkgs.installShellFiles]; + # disable tests + doCheck = false; + meta.mainProgram = "hx"; }; + cargoArtifacts = craneLibMSRV.buildDepsOnly commonArgs; in { - packages = rec { - helix = pkgs.callPackage ./default.nix {inherit gitRev;}; - - /** - The default Helix build. Uses the latest stable Rust toolchain, and unstable - nixpkgs. - - The build inputs can be overriden with the following: - - packages.${system}.default.override { rustPlatform = newPlatform; }; - - Overriding a derivation attribute can be done as well: - - packages.${system}.default.overrideAttrs { buildType = "debug"; }; - */ - default = helix; + packages = { + helix-unwrapped = craneLibStable.buildPackage (commonArgs + // { + cargoArtifacts = craneLibStable.buildDepsOnly commonArgs; + postInstall = '' + mkdir -p $out/share/applications $out/share/icons/hicolor/scalable/apps $out/share/icons/hicolor/256x256/apps + cp contrib/Helix.desktop $out/share/applications + cp logo.svg $out/share/icons/hicolor/scalable/apps/helix.svg + cp contrib/helix.png $out/share/icons/hicolor/256x256/apps + installShellCompletion contrib/completion/hx.{bash,fish,zsh} + ''; + # set git revision for nix flake builds, see 'git_hash' in helix-loader/build.rs + HELIX_NIX_BUILD_REV = self.rev or self.dirtyRev or null; + }); + helix = makeOverridableHelix self.packages.${system}.helix-unwrapped {}; + helix-cogs = helix-cogs; + default = self.packages.${system}.helix; }; - checks.helix = self.outputs.packages.${system}.helix.override { - buildType = "debug"; - rustPlatform = msrvPlatform; + checks = { + # Build the crate itself + inherit (self.packages.${system}) helix; + + clippy = craneLibMSRV.cargoClippy (commonArgs + // { + inherit cargoArtifacts; + cargoClippyExtraArgs = "--all-targets -- --deny warnings"; + }); + + fmt = craneLibMSRV.cargoFmt commonArgs; + + doc = craneLibMSRV.cargoDoc (commonArgs + // { + inherit cargoArtifacts; + }); + + test = craneLibMSRV.cargoTest (commonArgs + // { + inherit cargoArtifacts; + }); }; - # Devshell behavior is preserved. - devShells.default = let - commonRustFlagsEnv = "-C link-arg=-fuse-ld=lld -C target-cpu=native --cfg tokio_unstable"; - platformRustFlagsEnv = pkgs.lib.optionalString pkgs.stdenv.isLinux "-Clink-arg=-Wl,--no-rosegment"; - in - pkgs.mkShell - { - inputsFrom = [self.checks.${system}.helix]; - nativeBuildInputs = with pkgs; - [ - lld - cargo-flamegraph - rust-bin.nightly.latest.rust-analyzer - ] - ++ (lib.optional (stdenv.isx86_64 && stdenv.isLinux) cargo-tarpaulin) - ++ (lib.optional stdenv.isLinux lldb) - ++ (lib.optional stdenv.isDarwin darwin.apple_sdk.frameworks.CoreFoundation); - shellHook = '' - export RUST_BACKTRACE="1" - export RUSTFLAGS="''${RUSTFLAGS:-""} ${commonRustFlagsEnv} ${platformRustFlagsEnv}" - ''; - }; + devShells.default = pkgs.mkShell { + inputsFrom = builtins.attrValues self.checks.${system}; + nativeBuildInputs = with pkgs; + [lld_13 cargo-flamegraph rust-analyzer] + ++ (lib.optional (stdenv.isx86_64 && stdenv.isLinux) pkgs.cargo-tarpaulin) + ++ (lib.optional stdenv.isLinux pkgs.lldb) + ++ (lib.optional stdenv.isDarwin (with pkgs.darwin.apple_sdk.frameworks; + [CoreFoundation Security])); + shellHook = '' + export HELIX_RUNTIME="$PWD/runtime" + export RUST_BACKTRACE="1" + export RUSTFLAGS="''${RUSTFLAGS:-""} ${rustFlagsEnv}" + ''; + }; }) // { overlays.default = final: prev: { - helix = final.callPackage ./default.nix {inherit gitRev;}; + inherit (self.packages.${final.system}) helix; }; }; diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 10fb5a52cdee..2f1e2efa3c9b 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -14,6 +14,7 @@ homepage.workspace = true [features] unicode-lines = ["ropey/unicode_lines"] integration = [] +steel = ["dep:steel-core"] [dependencies] helix-stdx = { path = "../helix-stdx" } @@ -55,6 +56,7 @@ chrono = { version = "0.4", default-features = false, features = ["alloc", "std" textwrap = "0.16.2" +steel-core = { workspace = true, optional = true } nucleo.workspace = true parking_lot.workspace = true globset = "0.4.16" diff --git a/helix-core/src/command_line.rs b/helix-core/src/command_line.rs index 960b247dfbd2..d05ee53370cb 100644 --- a/helix-core/src/command_line.rs +++ b/helix-core/src/command_line.rs @@ -757,6 +757,13 @@ impl<'a> Args<'a> { } } + pub fn raw(positionals: Vec>) -> Self { + Self { + positionals, + ..Self::default() + } + } + /// Reads the next token out of the given parser. /// /// If the command's signature sets a maximum number of positionals (via `raw_after`) then @@ -1114,7 +1121,7 @@ mod test { assert_incomplete_tokens(r#"echo %{hello {{} world}"#, &["echo", "hello {{} world}"]); } - fn parse_signature<'a>( + pub fn parse_signature<'a>( input: &'a str, signature: Signature, ) -> Result, Box> { diff --git a/helix-core/src/extensions.rs b/helix-core/src/extensions.rs new file mode 100644 index 000000000000..6cdbda4e9fb3 --- /dev/null +++ b/helix-core/src/extensions.rs @@ -0,0 +1,263 @@ +#[cfg(feature = "steel")] +pub mod steel_implementations { + + use std::borrow::Cow; + + use steel::{ + gc::ShareableMut, + rvals::{as_underlying_type, Custom, SteelString}, + steel_vm::{builtin::BuiltInModule, register_fn::RegisterFn}, + SteelVal, + }; + + use helix_stdx::rope::RopeSliceExt; + + use crate::syntax::{AutoPairConfig, SoftWrap}; + + impl steel::rvals::Custom for crate::Position {} + impl steel::rvals::Custom for crate::Selection {} + impl steel::rvals::Custom for AutoPairConfig {} + impl steel::rvals::Custom for SoftWrap {} + + pub struct RopeyError(ropey::Error); + + impl steel::rvals::Custom for RopeyError {} + + impl From for RopeyError { + fn from(value: ropey::Error) -> Self { + Self(value) + } + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + enum RangeKind { + Char, + Byte, + } + + #[derive(Clone, PartialEq, Eq)] + pub struct SteelRopeSlice { + text: crate::Rope, + start: usize, + end: usize, + kind: RangeKind, + } + + impl Custom for SteelRopeSlice { + // `equal?` on two ropes should return true if they are the same + fn equality_hint(&self, other: &dyn steel::rvals::CustomType) -> bool { + if let Some(other) = as_underlying_type::(other) { + self == other + } else { + false + } + } + + fn equality_hint_general(&self, other: &steel::SteelVal) -> bool { + match other { + SteelVal::StringV(s) => self.to_slice() == s.as_str(), + SteelVal::Custom(c) => Self::equality_hint(&self, c.read().as_ref()), + + _ => false, + } + } + } + + impl SteelRopeSlice { + pub fn from_string(string: SteelString) -> Self { + Self { + text: crate::Rope::from_str(string.as_str()), + start: 0, + end: string.len(), + kind: RangeKind::Char, + } + } + + pub fn new(rope: crate::Rope) -> Self { + let end = rope.len_chars(); + Self { + text: rope, + start: 0, + end, + kind: RangeKind::Char, + } + } + + fn to_slice(&self) -> crate::RopeSlice<'_> { + match self.kind { + RangeKind::Char => self.text.slice(self.start..self.end), + RangeKind::Byte => self.text.byte_slice(self.start..self.end), + } + } + + pub fn line(mut self, cursor: usize) -> Result { + match self.kind { + RangeKind::Char => { + let slice = self.text.get_slice(self.start..self.end).ok_or_else(|| { + RopeyError(ropey::Error::CharIndexOutOfBounds(self.start, self.end)) + })?; + + // Move the start range, to wherever this lines up + let index = slice.try_line_to_char(cursor)?; + + let line = slice.line(cursor); + + self.start += index; + self.end = self.start + line.len_chars(); + + Ok(self) + } + RangeKind::Byte => { + let slice = + self.text + .get_byte_slice(self.start..self.end) + .ok_or_else(|| { + RopeyError(ropey::Error::ByteIndexOutOfBounds(self.start, self.end)) + })?; + + // Move the start range, to wherever this lines up + let index = slice.try_line_to_byte(cursor)?; + let line = slice.line(cursor); + + self.start += index; + self.end = self.start + line.len_bytes(); + + Ok(self) + } + } + } + + pub fn slice(mut self, lower: usize, upper: usize) -> Result { + match self.kind { + RangeKind::Char => { + self.end = self.start + upper; + self.start += lower; + + // Just check that this is legal + self.text.get_slice(self.start..self.end).ok_or_else(|| { + RopeyError(ropey::Error::CharIndexOutOfBounds(self.start, self.end)) + })?; + + Ok(self) + } + RangeKind::Byte => { + self.start = self.text.try_byte_to_char(self.start)? + lower; + self.end = self.start + (upper - lower); + + self.text + .get_byte_slice(self.start..self.end) + .ok_or_else(|| { + RopeyError(ropey::Error::ByteIndexOutOfBounds(self.start, self.end)) + })?; + + self.kind = RangeKind::Char; + Ok(self) + } + } + } + + pub fn byte_slice(mut self, lower: usize, upper: usize) -> Result { + match self.kind { + RangeKind::Char => { + self.start = self.text.try_char_to_byte(self.start)? + lower; + self.end = self.start + (upper - lower); + self.kind = RangeKind::Byte; + + // Just check that this is legal + self.text.get_slice(self.start..self.end).ok_or_else(|| { + RopeyError(ropey::Error::CharIndexOutOfBounds(self.start, self.end)) + })?; + + Ok(self) + } + RangeKind::Byte => { + self.start += lower; + self.end = self.start + (upper - lower); + + self.text + .get_byte_slice(self.start..self.end) + .ok_or_else(|| { + RopeyError(ropey::Error::ByteIndexOutOfBounds(self.start, self.end)) + })?; + + Ok(self) + } + } + } + + pub fn char_to_byte(&self, pos: usize) -> Result { + Ok(self.to_slice().try_char_to_byte(pos)?) + } + + pub fn to_string(&self) -> String { + self.to_slice().to_string() + } + + pub fn len_chars(&self) -> usize { + self.to_slice().len_chars() + } + + pub fn get_char(&self, index: usize) -> Option { + self.to_slice().get_char(index) + } + + pub fn len_lines(&self) -> usize { + self.to_slice().len_lines() + } + + pub fn trim_start(mut self) -> Self { + let slice = self.to_slice(); + + for (idx, c) in slice.chars().enumerate() { + if !c.is_whitespace() { + match self.kind { + RangeKind::Char => { + self.start += idx; + } + RangeKind::Byte => { + self.start += slice.char_to_byte(idx); + } + } + + break; + } + } + + self + } + + pub fn trimmed_starts_with(&self, pat: SteelString) -> bool { + let maybe_owned = Cow::from(self.to_slice()); + + maybe_owned.trim_start().starts_with(pat.as_str()) + } + + pub fn starts_with(&self, pat: SteelString) -> bool { + self.to_slice().starts_with(pat.as_str()) + } + + pub fn ends_with(&self, pat: SteelString) -> bool { + self.to_slice().ends_with(pat.as_str()) + } + } + + pub fn rope_module() -> BuiltInModule { + let mut module = BuiltInModule::new("helix/core/text"); + + module + .register_fn("string->rope", SteelRopeSlice::from_string) + .register_fn("rope->slice", SteelRopeSlice::slice) + .register_fn("rope-char->byte", SteelRopeSlice::char_to_byte) + .register_fn("rope->byte-slice", SteelRopeSlice::byte_slice) + .register_fn("rope->line", SteelRopeSlice::line) + .register_fn("rope->string", SteelRopeSlice::to_string) + .register_fn("rope-len-chars", SteelRopeSlice::len_chars) + .register_fn("rope-char-ref", SteelRopeSlice::get_char) + .register_fn("rope-len-lines", SteelRopeSlice::len_lines) + .register_fn("rope-starts-with?", SteelRopeSlice::starts_with) + .register_fn("rope-ends-with?", SteelRopeSlice::ends_with) + .register_fn("rope-trim-start", SteelRopeSlice::trim_start); + + module + } +} diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 3fcddfcd189a..b275a1f15f19 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -34,6 +34,8 @@ mod transaction; pub mod uri; pub mod wrap; +pub mod extensions; + pub mod unicode { pub use unicode_general_category as category; pub use unicode_segmentation as segmentation; diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 677cdfa0b673..aab07278d21f 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -174,7 +174,40 @@ pub struct LanguageConfiguration { pub persistent_diagnostic_sources: Vec, } -#[derive(Debug, PartialEq, Eq, Hash)] +impl Clone for LanguageConfiguration { + fn clone(&self) -> Self { + LanguageConfiguration { + language_id: self.language_id.clone(), + language_server_language_id: self.language_server_language_id.clone(), + scope: self.scope.clone(), + file_types: self.file_types.clone(), + shebangs: self.shebangs.clone(), + roots: self.roots.clone(), + comment_tokens: self.comment_tokens.clone(), + block_comment_tokens: self.block_comment_tokens.clone(), + text_width: self.text_width.clone(), + soft_wrap: self.soft_wrap.clone(), + auto_format: self.auto_format.clone(), + formatter: self.formatter.clone(), + diagnostic_severity: self.diagnostic_severity.clone(), + grammar: self.grammar.clone(), + injection_regex: self.injection_regex.clone(), + highlight_config: self.highlight_config.clone(), + language_servers: self.language_servers.clone(), + indent: self.indent.clone(), + indent_query: OnceCell::new(), + textobject_query: OnceCell::new(), + debugger: self.debugger.clone(), + auto_pairs: self.auto_pairs.clone(), + rulers: self.rulers.clone(), + workspace_lsp_roots: self.workspace_lsp_roots.clone(), + persistent_diagnostic_sources: self.persistent_diagnostic_sources.clone(), + path_completion: self.path_completion, + } + } +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone)] pub enum FileType { /// The extension of the file, either the `Path::extension` or the full /// filename if the file does not have an extension. @@ -378,7 +411,7 @@ enum LanguageServerFeatureConfiguration { Simple(String), } -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct LanguageServerFeatures { pub name: String, pub only: HashSet, @@ -457,7 +490,8 @@ where builder.build().map(Some).map_err(serde::de::Error::custom) } -#[derive(Debug, Serialize, Deserialize)] +// TODO: Remove clone once the configuration API is decided +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "kebab-case")] pub struct LanguageServerConfiguration { pub command: String, @@ -542,7 +576,7 @@ pub struct DebuggerQuirks { pub absolute_paths: bool, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "kebab-case")] pub struct IndentationConfiguration { #[serde(deserialize_with = "deserialize_tab_width")] @@ -861,7 +895,8 @@ pub struct SoftWrap { pub wrap_at_text_width: Option, } -#[derive(Debug)] +// TODO: Remove clone once the configuration API is decided +#[derive(Debug, Clone)] struct FileTypeGlob { glob: globset::Glob, language_id: usize, @@ -873,7 +908,8 @@ impl FileTypeGlob { } } -#[derive(Debug)] +// TODO: Remove clone once the configuration API is decided +#[derive(Debug, Clone)] struct FileTypeGlobMatcher { matcher: globset::GlobSet, file_types: Vec, @@ -903,18 +939,16 @@ impl FileTypeGlobMatcher { } // Expose loader as Lazy<> global since it's always static? - -#[derive(Debug)] +// TODO: Remove clone once the configuration API is decided +#[derive(Debug, Clone)] pub struct Loader { // highlight_names ? language_configs: Vec>, language_config_ids_by_extension: HashMap, // Vec language_config_ids_glob_matcher: FileTypeGlobMatcher, language_config_ids_by_shebang: HashMap, - language_server_configs: HashMap, - - scopes: ArcSwap>, + scopes: Arc>>, } pub type LoaderError = globset::Error; @@ -954,7 +988,8 @@ impl Loader { language_config_ids_glob_matcher: FileTypeGlobMatcher::new(file_type_globs)?, language_config_ids_by_shebang, language_server_configs: config.language_server, - scopes: ArcSwap::from_pointee(Vec::new()), + // TODO: Remove this once the configuration API is decided + scopes: Arc::new(ArcSwap::from_pointee(Vec::new())), }) } @@ -1058,6 +1093,12 @@ impl Loader { self.language_configs.iter() } + pub fn language_configs_mut( + &mut self, + ) -> impl Iterator> { + self.language_configs.iter_mut() + } + pub fn language_server_configs(&self) -> &HashMap { &self.language_server_configs } diff --git a/helix-event/Cargo.toml b/helix-event/Cargo.toml index 41f3b48369de..03a86730b17e 100644 --- a/helix-event/Cargo.toml +++ b/helix-event/Cargo.toml @@ -16,10 +16,10 @@ foldhash.workspace = true hashbrown = "0.15" tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] } # the event registry is essentially read only but must be an rwlock so we can -# setup new events on initialization, hardware-lock-elision hugely benefits this case -# as it essentially makes the lock entirely free as long as there is no writes +# setup new events on intalization, hardware-lock-elision hugnly benefits this case +# as is essentially makes the lock entirely free as long as there is no writes +once_cell = "1.20" parking_lot = { workspace = true, features = ["hardware-lock-elision"] } -once_cell = "1.21" anyhow = "1" log = "0.4" diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index f2b78a118ae9..215a2998a705 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -1534,4 +1534,63 @@ impl Client { changes, }) } + + // Everything below is explicitly extensions used for handling non standard lsp commands + pub fn non_standard_extension( + &self, + method_name: String, + params: Option, + ) -> Option>> { + Some(self.call_non_standard(DynamicLspRequest { + method_name, + params, + })) + } + + fn call_non_standard(&self, request: DynamicLspRequest) -> impl Future> { + self.call_non_standard_with_timeout(request, self.req_timeout) + } + + fn call_non_standard_with_timeout( + &self, + request: DynamicLspRequest, + timeout_secs: u64, + ) -> impl Future> { + let server_tx = self.server_tx.clone(); + let id = self.next_request_id(); + + let params = serde_json::to_value(&request.params); + async move { + use std::time::Duration; + use tokio::time::timeout; + + let request = jsonrpc::MethodCall { + jsonrpc: Some(jsonrpc::Version::V2), + id: id.clone(), + method: (&request.method_name).to_string(), + params: Self::value_into_params(params?), + }; + + let (tx, mut rx) = channel::>(1); + + server_tx + .send(Payload::Request { + chan: tx, + value: request, + }) + .map_err(|e| Error::Other(e.into()))?; + + // TODO: delay other calls until initialize success + timeout(Duration::from_secs(timeout_secs), rx.recv()) + .await + .map_err(|_| Error::Timeout(id))? // return Timeout + .ok_or(Error::StreamClosed)? + } + } +} + +#[derive(serde::Serialize, Deserialize)] +pub struct DynamicLspRequest { + method_name: String, + params: Option, } diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 9ea2d4589e46..8cf663a31068 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -31,10 +31,11 @@ assets = [ ] [features] -default = ["git"] +default = ["git", "steel"] # Remove steel if you don't want it unicode-lines = ["helix-core/unicode-lines", "helix-view/unicode-lines"] integration = ["helix-event/integration_test"] git = ["helix-vcs/git"] +steel = ["dep:steel-core", "helix-core/steel", "helix-view/steel", "tui/steel"] [[bin]] name = "hx" @@ -91,6 +92,11 @@ serde = { version = "1.0", features = ["derive"] } grep-regex = "0.1.13" grep-searcher = "0.1.14" +# plugin support +steel-core = { workspace = true, optional = true } +steel-doc = { git = "https://github.com/mattwparas/steel.git", version = "0.6.0" } +# steel-doc = { path = "/Users/matt/code/steel/crates/steel-doc", version = "0.6.0" } + [target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100 signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } libc = "0.2.171" diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 3bc32439565d..be6f75c8c518 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -234,7 +234,7 @@ impl Application { ]) .context("build signal handler")?; - let app = Self { + let mut app = Self { compositor, terminal, editor, @@ -244,6 +244,26 @@ impl Application { lsp_progress: LspProgressMap::new(), }; + { + // TODO: Revisit this! + let syn_loader = app.editor.syn_loader.clone(); + + let mut cx = crate::commands::Context { + register: None, + count: std::num::NonZeroUsize::new(1), + editor: &mut app.editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs: &mut app.jobs, + }; + + crate::commands::ScriptingEngine::run_initialization_script( + &mut cx, + app.config.clone(), + syn_loader, + ); + } + Ok(app) } @@ -334,6 +354,10 @@ impl Application { self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); self.render().await; } + Some(callback) = self.jobs.local_futures.next() => { + self.jobs.handle_local_callback(&mut self.editor, &mut self.compositor, callback); + self.render().await; + } event = self.editor.wait_event() => { let _idle_handled = self.handle_editor_event(event).await; @@ -370,6 +394,7 @@ impl Application { }; self.config.store(Arc::new(app_config)); } + ConfigEvent::Change => {} } // Update all the relevant members in the editor after updating diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 2e15dcdcc77c..06bda783b8f5 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1,4 +1,5 @@ pub(crate) mod dap; +pub(crate) mod engine; pub(crate) mod lsp; pub(crate) mod typed; @@ -11,16 +12,23 @@ use helix_stdx::{ }; use helix_vcs::{FileChange, Hunk}; pub use lsp::*; + +pub use engine::ScriptingEngine; + +#[cfg(feature = "steel")] +pub use engine::steel::{helix_module_file, steel_init_file}; + use tui::{ text::{Span, Spans}, - widgets::Cell, + widgets::{Cell, Row}, }; pub use typed::*; use helix_core::{ char_idx_at_visual_offset, chars::char_is_word, - command_line, comment, + command_line::{self, Args}, + comment, doc_formatter::TextFormat, encoding, find_workspace, graphemes::{self, next_grapheme_boundary}, @@ -244,6 +252,7 @@ impl MappableCommand { pub fn execute(&self, cx: &mut Context) { match &self { Self::Typable { name, args, doc: _ } => { + // TODO: Swap the order to allow overriding the existing commands? if let Some(command) = typed::TYPABLE_COMMAND_MAP.get(name.as_str()) { let mut cx = compositor::Context { editor: cx.editor, @@ -256,7 +265,11 @@ impl MappableCommand { cx.editor.set_error(format!("{}", e)); } } else { - cx.editor.set_error(format!("no such command: '{name}'")); + // TODO: Update this + let args = args.split_whitespace().map(Cow::from).collect(); + if !ScriptingEngine::call_function_by_name(cx, name, args) { + cx.editor.set_error(format!("no such command: '{name}'")); + } } } Self::Static { fun, .. } => (fun)(cx), @@ -296,6 +309,14 @@ impl MappableCommand { } } + pub(crate) fn doc_mut(&mut self) -> Option<&mut String> { + if let Self::Typable { doc, .. } = self { + Some(doc) + } else { + None + } + } + #[rustfmt::skip] static_commands!( no_op, "Do nothing", @@ -634,17 +655,17 @@ impl std::str::FromStr for MappableCommand { ensure!(!name.is_empty(), "Expected typable command name"); typed::TYPABLE_COMMAND_MAP .get(name) - .map(|cmd| { - let doc = if args.is_empty() { - cmd.doc.to_string() - } else { - format!(":{} {:?}", cmd.name, args) - }; - MappableCommand::Typable { - name: cmd.name.to_owned(), - doc, - args: args.to_string(), - } + .map(|cmd| MappableCommand::Typable { + name: cmd.name.to_owned(), + doc: format!(":{} {:?}", cmd.name, args), + args: args.to_owned(), + }) + .or_else(|| { + Some(MappableCommand::Typable { + name: name.to_owned(), + args: args.to_owned(), + doc: "Undocumented plugin command".to_string(), + }) }) .ok_or_else(|| anyhow!("No TypableCommand named '{}'", s)) } else if let Some(suffix) = s.strip_prefix('@') { @@ -3119,6 +3140,7 @@ fn buffer_picker(cx: &mut Context) { struct BufferMeta { id: DocumentId, path: Option, + name: Option, is_modified: bool, is_current: bool, focused_at: std::time::Instant, @@ -3127,6 +3149,7 @@ fn buffer_picker(cx: &mut Context) { let new_meta = |doc: &Document| BufferMeta { id: doc.id(), path: doc.path().cloned(), + name: doc.name.clone(), is_modified: doc.is_modified(), is_current: doc.id() == current, focused_at: doc.focused_at, @@ -4080,6 +4103,18 @@ pub mod insert { helix_event::dispatch(PostInsertChar { c, cx }); } + pub fn insert_string(cx: &mut Context, string: String) { + let (view, doc) = current!(cx.editor); + + let indent = Tendril::from(string); + let transaction = Transaction::insert( + doc.text(), + &doc.selection(view.id).clone().cursors(doc.text().slice(..)), + indent, + ); + doc.apply(&transaction, view.id); + } + pub fn smart_tab(cx: &mut Context) { let (view, doc) = current_ref!(cx.editor); let view_id = view.id; diff --git a/helix-term/src/commands/engine.rs b/helix-term/src/commands/engine.rs new file mode 100644 index 000000000000..6a5abbfd4bf9 --- /dev/null +++ b/helix-term/src/commands/engine.rs @@ -0,0 +1,223 @@ +use arc_swap::{ArcSwap, ArcSwapAny}; +use helix_core::syntax; +use helix_view::{document::Mode, input::KeyEvent}; + +use std::{borrow::Cow, sync::Arc}; + +use crate::{ + compositor, + config::Config, + keymap::KeymapResult, + ui::{self, PromptEvent}, +}; + +use super::{Context, MappableCommand, TYPABLE_COMMAND_LIST}; + +#[cfg(feature = "steel")] +mod components; + +#[cfg(feature = "steel")] +pub mod steel; + +pub enum PluginSystemKind { + None, + #[cfg(feature = "steel")] + Steel, +} + +pub enum PluginSystemTypes { + None(NoEngine), + #[cfg(feature = "steel")] + Steel(steel::SteelScriptingEngine), +} + +// The order in which the plugins will be evaluated against - if we wanted to include, lets say `rhai`, +// we would have to order the precedence for searching for exported commands, or somehow merge them? +const PLUGIN_PRECEDENCE: &[PluginSystemTypes] = &[ + #[cfg(feature = "steel")] + PluginSystemTypes::Steel(steel::SteelScriptingEngine), + PluginSystemTypes::None(NoEngine), +]; + +pub struct NoEngine; + +// This will be the boundary layer between the editor and the engine. +pub struct ScriptingEngine; + +// Macro to automatically dispatch to hopefully get some inlining +macro_rules! manual_dispatch { + ($kind:expr, $raw:tt ($($args:expr),* $(,)?) ) => { + match $kind { + PluginSystemTypes::None(n) => n.$raw($($args),*), + #[cfg(feature = "steel")] + PluginSystemTypes::Steel(s) => s.$raw($($args),*), + } + }; +} + +impl ScriptingEngine { + pub fn initialize() { + for kind in PLUGIN_PRECEDENCE { + manual_dispatch!(kind, initialize()) + } + } + + pub fn run_initialization_script( + cx: &mut Context, + configuration: Arc>>, + language_configuration: Arc>, + ) { + for kind in PLUGIN_PRECEDENCE { + manual_dispatch!( + kind, + run_initialization_script( + cx, + configuration.clone(), + language_configuration.clone() + ) + ) + } + } + + pub fn handle_keymap_event( + editor: &mut ui::EditorView, + mode: Mode, + cxt: &mut Context, + event: KeyEvent, + ) -> Option { + for kind in PLUGIN_PRECEDENCE { + let res = manual_dispatch!(kind, handle_keymap_event(editor, mode, cxt, event)); + + if res.is_some() { + return res; + } + } + + None + } + + pub fn call_function_by_name(cx: &mut Context, name: &str, args: Vec>) -> bool { + for kind in PLUGIN_PRECEDENCE { + if manual_dispatch!(kind, call_function_by_name(cx, name, &args)) { + return true; + } + } + + false + } + + pub fn call_typed_command<'a>( + cx: &mut compositor::Context, + command: &'a str, + parts: &'a [&'a str], + event: PromptEvent, + ) -> bool { + for kind in PLUGIN_PRECEDENCE { + if manual_dispatch!(kind, call_typed_command(cx, command, parts, event)) { + return true; + } + } + + false + } + + pub fn get_doc_for_identifier(ident: &str) -> Option { + for kind in PLUGIN_PRECEDENCE { + let doc = manual_dispatch!(kind, get_doc_for_identifier(ident)); + + if doc.is_some() { + return doc; + } + } + + None + } + + pub fn available_commands<'a>() -> Vec> { + PLUGIN_PRECEDENCE + .iter() + .flat_map(|kind| manual_dispatch!(kind, available_commands())) + .collect() + } + + pub fn generate_sources() { + for kind in PLUGIN_PRECEDENCE { + manual_dispatch!(kind, generate_sources()) + } + } +} + +impl PluginSystem for NoEngine { + fn engine_name(&self) -> PluginSystemKind { + PluginSystemKind::None + } +} + +/// These methods are the main entry point for interaction with the rest of +/// the editor system. +pub trait PluginSystem { + /// If any initialization needs to happen prior to the initialization script being run, + /// this is done here. This is run before the context is available. + fn initialize(&self) {} + + fn engine_name(&self) -> PluginSystemKind; + + /// Post initialization, once the context is available. This means you should be able to + /// run anything here that could modify the context before the main editor is available. + fn run_initialization_script( + &self, + _cx: &mut Context, + _configuration: Arc>>, + _language_configuration: Arc>, + ) { + } + + /// Allow the engine to directly handle a keymap event. This is some of the tightest integration + /// with the engine, directly intercepting any keymap events. By default, this just delegates to the + /// editors default keybindings. + #[inline(always)] + fn handle_keymap_event( + &self, + _editor: &mut ui::EditorView, + _mode: Mode, + _cxt: &mut Context, + _event: KeyEvent, + ) -> Option { + None + } + + /// This attempts to call a function in the engine with the name `name` using the args `args`. The context + /// is available here. Returns a bool indicating whether the function exists or not. + #[inline(always)] + fn call_function_by_name(&self, _cx: &mut Context, _name: &str, _args: &[Cow]) -> bool { + false + } + + /// This is explicitly for calling a function via the typed command interface, e.g. `:vsplit`. The context here + /// that is available is more limited than the context available in `call_function_if_global_exists`. This also + /// gives the ability to handle in progress commands with `PromptEvent`. + #[inline(always)] + fn call_typed_command<'a>( + &self, + _cx: &mut compositor::Context, + _input: &'a str, + _parts: &'a [&'a str], + _event: PromptEvent, + ) -> bool { + false + } + + /// Given an identifier, extract the documentation from the engine. + #[inline(always)] + fn get_doc_for_identifier(&self, _ident: &str) -> Option { + None + } + + /// Fuzzy match the input against the fuzzy matcher, used for handling completions on typed commands + #[inline(always)] + fn available_commands<'a>(&self) -> Vec> { + Vec::new() + } + + fn generate_sources(&self) {} +} diff --git a/helix-term/src/commands/engine/components.rs b/helix-term/src/commands/engine/components.rs new file mode 100644 index 000000000000..fb11f16a991b --- /dev/null +++ b/helix-term/src/commands/engine/components.rs @@ -0,0 +1,855 @@ +use std::{collections::HashMap, sync::Arc}; + +use helix_core::Position; +use helix_view::{ + graphics::{Color, CursorKind, Rect, UnderlineStyle}, + input::{Event, KeyEvent, MouseButton, MouseEvent}, + keyboard::{KeyCode, KeyModifiers}, + theme::{Modifier, Style}, + Editor, +}; +use steel::{ + rvals::{as_underlying_type, Custom, FromSteelVal, IntoSteelVal, SteelString}, + steel_vm::{builtin::BuiltInModule, engine::Engine, register_fn::RegisterFn}, + SteelVal, +}; +use tokio::sync::Mutex; +use tui::{ + buffer::Buffer, + text::Text, + widgets::{self, Block, BorderType, Borders, ListItem, Widget}, +}; + +use crate::{ + commands::{engine::steel::BoxDynComponent, Context}, + compositor::{self, Component}, + ui::overlay::overlaid, +}; + +use super::steel::{enter_engine, present_error_inside_engine_context, WrappedDynComponent}; + +#[derive(Clone)] +struct AsyncReader { + // Take that, and write it back to a terminal session that is + // getting rendered. + channel: Arc>>, +} + +impl AsyncReader { + async fn read_line(self) -> Option { + let mut buf = String::new(); + + let mut guard = self.channel.lock().await; + + while let Ok(v) = guard.try_recv() { + buf.push_str(&v); + } + + let fut = guard.recv(); + + // If we haven't found any characters, just wait until we have something. + // Otherwise, we give this a 2 ms buffer to check if more things are + // coming through the pipe. + if buf.is_empty() { + let next = fut.await; + + match next { + Some(v) => { + buf.push_str(&v); + Some(buf) + } + None => None, + } + } else { + match tokio::time::timeout(std::time::Duration::from_millis(2), fut).await { + Ok(Some(v)) => { + buf.push_str(&v); + Some(buf) + } + Ok(None) => { + if buf.is_empty() { + None + } else { + Some(buf) + } + } + Err(_) => Some(buf), + } + } + } +} + +impl Custom for AsyncReader {} + +struct AsyncWriter { + channel: tokio::sync::mpsc::UnboundedSender, +} + +impl std::io::Write for AsyncWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + if let Err(_) = self.channel.send(String::from_utf8_lossy(buf).to_string()) { + Ok(0) + } else { + Ok(buf.len()) + } + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +// TODO: Move the main configuration function to use this instead +pub fn helix_component_module() -> BuiltInModule { + let mut module = BuiltInModule::new("helix/components"); + + module + .register_fn("async-read-line", AsyncReader::read_line) + // TODO: + .register_fn("make-async-reader-writer", || { + let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); + + let writer = AsyncWriter { channel: sender }; + let reader = AsyncReader { + channel: Arc::new(Mutex::new(receiver)), + }; + + vec![ + SteelVal::new_dyn_writer_port(writer), + reader.into_steelval().unwrap(), + ] + }) + // Attempt to pop off a specific component + .register_fn( + "pop-dynamic-component-by-name", + |ctx: &mut Context, name: SteelString| { + // Removing a component by name here will be important! + todo!() + }, + ) + .register_fn("theme->bg", |ctx: &mut Context| { + ctx.editor.theme.get("ui.background") + }) + .register_fn("theme->fg", |ctx: &mut Context| { + ctx.editor.theme.get("ui.text") + }) + .register_fn("buffer-area", |buffer: &mut Buffer| buffer.area) + .register_fn("frame-set-string!", buffer_set_string) + .register_fn("new-component!", SteelDynamicComponent::new_dyn) + .register_fn("position", Position::new) + .register_fn("position-row", |position: &Position| position.row) + .register_fn("position-col", |position: &Position| position.col) + .register_fn( + "set-position-row!", + |position: &mut Position, row: usize| { + position.row = row; + }, + ) + .register_fn( + "set-position-col!", + |position: &mut Position, col: usize| { + position.col = col; + }, + ) + .register_fn("area", helix_view::graphics::Rect::new) + .register_fn("area-x", |area: &helix_view::graphics::Rect| area.x) + .register_fn("area-y", |area: &helix_view::graphics::Rect| area.y) + .register_fn("area-width", |area: &helix_view::graphics::Rect| area.width) + .register_fn("area-height", |area: &helix_view::graphics::Rect| { + area.height + }) + .register_fn("overlaid", |component: &mut WrappedDynComponent| { + let inner: Option> = + component.inner.take().map(|x| { + Box::new(overlaid(BoxDynComponent::new(x))) + as Box + }); + + component.inner = inner; + }) + .register_fn("widget/list", |items: Vec| { + widgets::List::new( + items + .into_iter() + .map(|x| ListItem::new(Text::from(x))) + .collect::>(), + ) + }) + // Pass references in as well? + .register_fn( + "widget/list/render", + |buf: &mut Buffer, area: Rect, list: widgets::List| list.render(area, buf), + ) + .register_fn("block", || { + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::White)) + .border_type(BorderType::Rounded) + .style(Style::default().bg(Color::Black)) + }) + // TODO: Expose these accordingly + .register_fn( + "make-block", + |style: Style, border_style: Style, borders: SteelString, border_type: SteelString| { + let border_type = match border_type.as_str() { + "plain" => BorderType::Plain, + "rounded" => BorderType::Rounded, + "double" => BorderType::Double, + "thick" => BorderType::Thick, + _ => BorderType::Plain, + }; + + let borders = match borders.as_str() { + "top" => Borders::TOP, + "left" => Borders::LEFT, + "right" => Borders::RIGHT, + "bottom" => Borders::BOTTOM, + "all" => Borders::ALL, + _ => Borders::empty(), + }; + + Block::default() + .borders(borders) + .border_style(border_style) + .border_type(border_type) + .style(style) + }, + ) + .register_fn( + "block/render", + |buf: &mut Buffer, area: Rect, block: Block| block.render(area, buf), + ) + .register_fn("buffer/clear", Buffer::clear) + .register_fn("buffer/clear-with", Buffer::clear_with) + // Mutate a color in place, to save some headache. + .register_fn( + "set-color-rgb!", + |color: &mut Color, r: u8, g: u8, b: u8| { + *color = Color::Rgb(r, g, b); + }, + ) + .register_fn("set-color-indexed!", |color: &mut Color, index: u8| { + *color = Color::Indexed(index); + }) + .register_value("Color/Reset", Color::Reset.into_steelval().unwrap()) + .register_value("Color/Black", Color::Black.into_steelval().unwrap()) + .register_value("Color/Red", Color::Red.into_steelval().unwrap()) + .register_value("Color/White", Color::White.into_steelval().unwrap()) + .register_value("Color/Green", Color::Green.into_steelval().unwrap()) + .register_value("Color/Yellow", Color::Yellow.into_steelval().unwrap()) + .register_value("Color/Blue", Color::Blue.into_steelval().unwrap()) + .register_value("Color/Magenta", Color::Magenta.into_steelval().unwrap()) + .register_value("Color/Cyan", Color::Cyan.into_steelval().unwrap()) + .register_value("Color/Gray", Color::Gray.into_steelval().unwrap()) + .register_value("Color/LightRed", Color::LightRed.into_steelval().unwrap()) + .register_value( + "Color/LightGreen", + Color::LightGreen.into_steelval().unwrap(), + ) + .register_value( + "Color/LightYellow", + Color::LightYellow.into_steelval().unwrap(), + ) + .register_value("Color/LightBlue", Color::LightBlue.into_steelval().unwrap()) + .register_value( + "Color/LightMagenta", + Color::LightMagenta.into_steelval().unwrap(), + ) + .register_value("Color/LightCyan", Color::LightCyan.into_steelval().unwrap()) + .register_value("Color/LightGray", Color::LightGray.into_steelval().unwrap()) + .register_fn("Color/rgb", Color::Rgb) + .register_fn("Color-red", Color::red) + .register_fn("Color-green", Color::green) + .register_fn("Color-blue", Color::blue) + .register_fn("Color/Indexed", Color::Indexed) + .register_fn("set-style-fg!", |style: &mut Style, color: Color| { + style.fg = Some(color); + }) + .register_fn("style-fg", Style::fg) + .register_fn("style-bg", Style::bg) + .register_fn("style-with-italics", |style: &Style| { + let patch = Style::default().add_modifier(Modifier::ITALIC); + style.patch(patch) + }) + .register_fn("style-with-bold", |style: Style| { + let patch = Style::default().add_modifier(Modifier::BOLD); + style.patch(patch) + }) + .register_fn("style-with-dim", |style: &Style| { + let patch = Style::default().add_modifier(Modifier::DIM); + style.patch(patch) + }) + .register_fn("style-with-slow-blink", |style: Style| { + let patch = Style::default().add_modifier(Modifier::SLOW_BLINK); + style.patch(patch) + }) + .register_fn("style-with-rapid-blink", |style: Style| { + let patch = Style::default().add_modifier(Modifier::RAPID_BLINK); + style.patch(patch) + }) + .register_fn("style-with-reversed", |style: Style| { + let patch = Style::default().add_modifier(Modifier::REVERSED); + style.patch(patch) + }) + .register_fn("style-with-hidden", |style: Style| { + let patch = Style::default().add_modifier(Modifier::HIDDEN); + style.patch(patch) + }) + .register_fn("style-with-crossed-out", |style: Style| { + let patch = Style::default().add_modifier(Modifier::CROSSED_OUT); + style.patch(patch) + }) + .register_fn("style->fg", |style: &Style| style.fg) + .register_fn("style->bg", |style: &Style| style.bg) + .register_fn("set-style-bg!", |style: &mut Style, color: Color| { + style.bg = Some(color); + }) + .register_fn("style-underline-color", Style::underline_color) + .register_fn("style-underline-style", Style::underline_style) + .register_value( + "Underline/Reset", + UnderlineStyle::Reset.into_steelval().unwrap(), + ) + .register_value( + "Underline/Line", + UnderlineStyle::Line.into_steelval().unwrap(), + ) + .register_value( + "Underline/Curl", + UnderlineStyle::Curl.into_steelval().unwrap(), + ) + .register_value( + "Underline/Dotted", + UnderlineStyle::Dotted.into_steelval().unwrap(), + ) + .register_value( + "Underline/Dashed", + UnderlineStyle::Dashed.into_steelval().unwrap(), + ) + .register_value( + "Underline/DoubleLine", + UnderlineStyle::DoubleLine.into_steelval().unwrap(), + ) + .register_fn("style", || Style::default()) + .register_value( + "event-result/consume", + SteelEventResult::Consumed.into_steelval().unwrap(), + ) + .register_value( + "event-result/consume-without-rerender", + SteelEventResult::ConsumedWithoutRerender + .into_steelval() + .unwrap(), + ) + .register_value( + "event-result/ignore", + SteelEventResult::Ignored.into_steelval().unwrap(), + ) + .register_value( + "event-result/close", + SteelEventResult::Close.into_steelval().unwrap(), + ) + // TODO: Use a reference here instead of passing by value. + .register_fn("key-event-char", |event: Event| { + if let Event::Key(event) = event { + event.char() + } else { + None + } + }) + .register_fn("key-event-modifier", |event: Event| { + if let Event::Key(KeyEvent { modifiers, .. }) = event { + Some(modifiers.bits()) + } else { + None + } + }) + .register_value( + "key-modifier-ctrl", + SteelVal::IntV(KeyModifiers::CONTROL.bits() as isize), + ) + .register_value( + "key-modifier-shift", + SteelVal::IntV(KeyModifiers::SHIFT.bits() as isize), + ) + .register_value( + "key-modifier-alt", + SteelVal::IntV(KeyModifiers::ALT.bits() as isize), + ) + .register_fn("key-event-F?", |event: Event, number: u8| match event { + Event::Key(KeyEvent { + code: KeyCode::F(x), + .. + }) if number == x => true, + _ => false, + }) + .register_fn("mouse-event?", |event: Event| { + matches!(event, Event::Mouse(_)) + }) + .register_fn("event-mouse-kind", |event: Event| { + if let Event::Mouse(MouseEvent { kind, .. }) = event { + match kind { + helix_view::input::MouseEventKind::Down(MouseButton::Left) => 0, + helix_view::input::MouseEventKind::Down(MouseButton::Right) => 1, + helix_view::input::MouseEventKind::Down(MouseButton::Middle) => 2, + helix_view::input::MouseEventKind::Up(MouseButton::Left) => 3, + helix_view::input::MouseEventKind::Up(MouseButton::Right) => 4, + helix_view::input::MouseEventKind::Up(MouseButton::Middle) => 5, + helix_view::input::MouseEventKind::Drag(MouseButton::Left) => 6, + helix_view::input::MouseEventKind::Drag(MouseButton::Right) => 7, + helix_view::input::MouseEventKind::Drag(MouseButton::Middle) => 8, + helix_view::input::MouseEventKind::Moved => 9, + helix_view::input::MouseEventKind::ScrollDown => 10, + helix_view::input::MouseEventKind::ScrollUp => 11, + helix_view::input::MouseEventKind::ScrollLeft => 12, + helix_view::input::MouseEventKind::ScrollRight => 13, + } + .into_steelval() + } else { + false.into_steelval() + } + }) + .register_fn("event-mouse-row", |event: Event| { + if let Event::Mouse(MouseEvent { row, .. }) = event { + row.into_steelval() + } else { + false.into_steelval() + } + }) + .register_fn("event-mouse-col", |event: Event| { + if let Event::Mouse(MouseEvent { column, .. }) = event { + column.into_steelval() + } else { + false.into_steelval() + } + }) + // Is this mouse event within the area provided + .register_fn("mouse-event-within-area?", |event: Event, area: Rect| { + if let Event::Mouse(MouseEvent { row, column, .. }) = event { + column > area.x + && column < area.x + area.width + && row > area.y + && row < area.y + area.height + } else { + false + } + }); + + macro_rules! register_key_events { + ($ ( $name:expr => $key:tt ) , *, ) => { + $( + module.register_fn(concat!("key-event-", $name, "?"), |event: Event| { + matches!( + event, + Event::Key( + KeyEvent { + code: KeyCode::$key, + .. + } + )) + }); + )* + }; + } + + // Key events for individual key codes + register_key_events!( + "escape" => Esc, + "backspace" => Backspace, + "enter" => Enter, + "left" => Left, + "right" => Right, + "up" => Up, + "down" => Down, + "home" => Home, + "page-up" => PageUp, + "page-down" => PageDown, + "tab" => Tab, + "delete" => Delete, + "insert" => Insert, + "null" => Null, + "caps-lock" => CapsLock, + "scroll-lock" => ScrollLock, + "num-lock" => NumLock, + "print-screen" => PrintScreen, + "pause" => Pause, + "menu" => Menu, + "keypad-begin" => KeypadBegin, + ); + + module +} + +// fn buffer_set_string( +// buffer: &mut tui::buffer::Buffer, +// x: u16, +// y: u16, +// string: steel::rvals::SteelString, +// style: Style, +// ) { +// buffer.set_string(x, y, string.as_str(), style) +// } + +fn buffer_set_string( + buffer: &mut tui::buffer::Buffer, + x: u16, + y: u16, + string: SteelVal, + style: Style, +) -> steel::rvals::Result<()> { + match string { + SteelVal::StringV(string) => { + buffer.set_string(x, y, string.as_str(), style); + Ok(()) + } + SteelVal::Custom(c) => { + if let Some(string) = + as_underlying_type::(c.read().as_ref()) + { + buffer.set_string(x, y, string.string.as_str(), style); + Ok(()) + } else { + steel::stop!(TypeMismatch => "buffer-set-string! expected a string") + } + } + _ => { + steel::stop!(TypeMismatch => "buffer-set-string! expected a string") + } + } + + // buffer.set_string(x, y, string.as_str(), style) +} + +/// A dynamic component, used for rendering +#[derive(Clone)] +pub struct SteelDynamicComponent { + // TODO: currently the component id requires using a &'static str, + // however in a world with dynamic components that might not be + // the case anymore + name: String, + // This _should_ be a struct, but in theory can be whatever you want. It will be the first argument + // passed to the functions in the remainder of the struct. + state: SteelVal, + handle_event: Option, + should_update: Option, + render: SteelVal, + cursor: Option, + required_size: Option, + + // Cached key event; we keep this around so that when sending + // events to the event handler, we can reuse the heap allocation + // instead of re-allocating for every event (which might be a lot) + key_event: Option, +} + +impl SteelDynamicComponent { + pub fn new( + name: String, + state: SteelVal, + render: SteelVal, + h: HashMap, + ) -> Self { + Self { + name, + state, + render, + handle_event: h.get("handle_event").cloned(), + should_update: h.get("should_update").cloned(), + cursor: h.get("cursor").cloned(), + required_size: h.get("required_size").cloned(), + key_event: None, + } + } + + pub fn new_dyn( + name: String, + state: SteelVal, + render: SteelVal, + h: HashMap, + ) -> WrappedDynComponent { + let s = Self::new(name, state, render, h); + + // TODO: Add guards here for the + WrappedDynComponent { + inner: Some(Box::new(s)), + } + } + + pub fn get_state(&self) -> SteelVal { + self.state.clone() + } + + pub fn get_render(&self) -> SteelVal { + self.render.clone() + } + + pub fn get_handle_event(&self) -> Option { + self.handle_event.clone() + } + + pub fn get_should_update(&self) -> Option { + self.should_update.clone() + } + + pub fn get_cursor(&self) -> Option { + self.cursor.clone() + } + + pub fn get_required_size(&self) -> Option { + self.required_size.clone() + } +} + +impl Custom for SteelDynamicComponent {} + +impl Custom for Box {} + +#[derive(Clone)] +enum SteelEventResult { + Consumed, + Ignored, + Close, + ConsumedWithoutRerender, +} + +impl Custom for SteelEventResult {} + +impl Component for SteelDynamicComponent { + fn name(&self) -> Option<&str> { + Some(&self.name) + } + + fn render( + &mut self, + area: helix_view::graphics::Rect, + frame: &mut tui::buffer::Buffer, + ctx: &mut compositor::Context, + ) { + let mut ctx = Context { + register: None, + count: None, + editor: ctx.editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs: ctx.jobs, + }; + + // Pass the `state` object through - this can be used for storing the state of whatever plugin thing we're + // attempting to render + let thunk = |engine: &mut Engine, f| { + engine.call_function_with_args_from_mut_slice( + self.render.clone(), + &mut [self.state.clone(), area.into_steelval().unwrap(), f], + ) + }; + + enter_engine(|guard| { + if let Err(e) = guard + .with_mut_reference::(frame) + .with_mut_reference::(&mut ctx) + .consume(|engine, args| { + let mut arg_iter = args.into_iter(); + + let buffer = arg_iter.next().unwrap(); + let context = arg_iter.next().unwrap(); + + engine.update_value("*helix.cx*", context); + + (thunk)(engine, buffer) + }) + { + present_error_inside_engine_context(&mut ctx, guard, e) + } + }) + } + + // TODO: Pass in event as well? Need to have immutable reference type + // Otherwise, we're gonna be in a bad spot. For now - just clone the object and pass it through. + // Clong is _not_ ideal, but it might be all we can do for now. + fn handle_event( + &mut self, + event: &Event, + ctx: &mut compositor::Context, + ) -> compositor::EventResult { + if let Some(handle_event) = &mut self.handle_event { + let mut ctx = Context { + register: None, + count: None, + editor: ctx.editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs: ctx.jobs, + }; + + match self.key_event.as_mut() { + Some(SteelVal::Custom(key_event)) => { + // Save the headache, reuse the allocation + if let Some(inner) = + steel::rvals::as_underlying_type_mut::(key_event.write().as_mut()) + { + *inner = event.clone(); + } + } + + None => { + self.key_event = Some(event.clone().into_steelval().unwrap()); + } + _ => { + panic!("This event needs to stay as a steelval"); + } + } + + // Pass the `state` object through - this can be used for storing the state of whatever plugin thing we're + // attempting to render + let thunk = |engine: &mut Engine| { + engine.call_function_with_args_from_mut_slice( + handle_event.clone(), + &mut [self.state.clone(), self.key_event.clone().unwrap()], + ) + }; + + let close_fn = compositor::EventResult::Consumed(Some(Box::new( + |compositor: &mut compositor::Compositor, _| { + // remove the layer + compositor.pop(); + }, + ))); + + // let event = match event { + // Event::Key(event) => *event, + // _ => return compositor::EventResult::Ignored(None), + // }; + + match enter_engine(|guard| { + guard + .with_mut_reference::(&mut ctx) + .consume(move |engine, arguments| { + let context = arguments[0].clone(); + engine.update_value("*helix.cx*", context); + + thunk(engine) + }) + }) { + Ok(v) => { + let value = SteelEventResult::from_steelval(&v); + + match value { + Ok(SteelEventResult::Close) => close_fn, + Ok(SteelEventResult::Consumed) => compositor::EventResult::Consumed(None), + Ok(SteelEventResult::ConsumedWithoutRerender) => { + compositor::EventResult::ConsumedWithoutRerender + } + Ok(SteelEventResult::Ignored) => compositor::EventResult::Ignored(None), + _ => match event { + // ctrl!('c') | key!(Esc) => close_fn, + _ => compositor::EventResult::Ignored(None), + }, + } + } + Err(e) => { + // Present the error + enter_engine(|x| present_error_inside_engine_context(&mut ctx, x, e)); + + compositor::EventResult::Ignored(None) + } + } + } else { + compositor::EventResult::Ignored(None) + } + } + + fn should_update(&self) -> bool { + true + + // if let Some(should_update) = &self.should_update { + // match ENGINE.with(|x| { + // let res = x + // .borrow_mut() + // .call_function_with_args(should_update.clone(), vec![self.state.clone()]); + + // res + // }) { + // Ok(v) => bool::from_steelval(&v).unwrap_or(true), + // Err(_) => true, + // } + // } else { + // true + // } + } + + // TODO: Implement immutable references. Right now I'm only supporting mutable references. + fn cursor( + &self, + area: helix_view::graphics::Rect, + _ctx: &Editor, + ) -> ( + Option, + helix_view::graphics::CursorKind, + ) { + if let Some(cursor) = &self.cursor { + // Pass the `state` object through - this can be used for storing the state of whatever plugin thing we're + // attempting to render + let thunk = |engine: &mut Engine| { + engine.call_function_with_args_from_mut_slice( + cursor.clone(), + &mut [self.state.clone(), area.into_steelval().unwrap()], + ) + }; + + let result = + Option::::from_steelval(&enter_engine(|x| thunk(x).unwrap())); + + match result { + Ok(v) => (v, CursorKind::Block), + // TODO: Figure out how to pop up an error message + Err(_e) => { + log::info!("Error: {:?}", _e); + (None, CursorKind::Block) + } + } + } else { + (None, helix_view::graphics::CursorKind::Hidden) + } + } + + fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { + // let name = self.type_name(); + + if let Some(required_size) = &mut self.required_size { + // log::info!("Calling required-size inside: {}", name); + + // TODO: Create some token that we can grab to enqueue function calls internally. Referencing + // the external API would cause problems - we just need to include a handle to the interpreter + // instance. Something like: + // ENGINE.call_function_or_enqueue? OR - this is the externally facing render function. Internal + // render calls do _not_ go through this interface. Instead, they are just called directly. + // + // If we go through this interface, we're going to get an already borrowed mut error, since it is + // re-entrant attempting to grab the ENGINE instead mutably, since we have to break the recursion + // somehow. By putting it at the edge, we then say - hey for these functions on this interface, + // call the engine instance. Otherwise, all computation happens inside the engine. + match enter_engine(|x| { + x.call_function_with_args_from_mut_slice( + required_size.clone(), + &mut [self.state.clone(), viewport.into_steelval().unwrap()], + ) + }) + .and_then(|x| Option::<(u16, u16)>::from_steelval(&x)) + { + Ok(v) => v, + // TODO: Figure out how to present an error + Err(_e) => None, + } + } else { + None + } + } + + fn type_name(&self) -> &'static str { + std::any::type_name::() + } + + fn id(&self) -> Option<&'static str> { + None + } +} diff --git a/helix-term/src/commands/engine/steel.rs b/helix-term/src/commands/engine/steel.rs new file mode 100644 index 000000000000..ceecf99a0fcb --- /dev/null +++ b/helix-term/src/commands/engine/steel.rs @@ -0,0 +1,3478 @@ +use arc_swap::{ArcSwap, ArcSwapAny}; +use crossterm::event::{Event, KeyCode, KeyModifiers}; +use helix_core::{ + command_line::Args, + diagnostic::Severity, + extensions::steel_implementations::{rope_module, SteelRopeSlice}, + find_workspace, graphemes, + syntax::{self, AutoPairConfig, IndentationConfiguration, LanguageConfiguration, SoftWrap}, + Range, Selection, Tendril, +}; +use helix_event::register_hook; +use helix_view::{ + annotations::diagnostics::DiagnosticFilter, + document::Mode, + editor::{ + Action, AutoSave, BufferLine, ConfigEvent, CursorShapeConfig, FilePickerConfig, + GutterConfig, IndentGuidesConfig, LineEndingConfig, LineNumber, LspConfig, SearchConfig, + SmartTabConfig, StatusLineConfig, TerminalConfig, WhitespaceConfig, + }, + events::{DocumentDidOpen, DocumentFocusLost, SelectionDidChange}, + extension::document_id_to_usize, + input::KeyEvent, + theme::Color, + DocumentId, Editor, Theme, ViewId, +}; +use once_cell::sync::{Lazy, OnceCell}; +use steel::{ + gc::{unsafe_erased_pointers::CustomReference, ShareableMut}, + rvals::{as_underlying_type, IntoSteelVal, SteelString}, + steel_vm::{ + engine::Engine, mutex_lock, mutex_unlock, register_fn::RegisterFn, ThreadStateController, + }, + steelerr, SteelErr, SteelVal, +}; + +use std::sync::Arc; +use std::{ + borrow::Cow, + collections::HashMap, + error::Error, + path::PathBuf, + sync::{atomic::AtomicBool, Mutex, MutexGuard}, + time::Duration, +}; + +use steel::{rvals::Custom, steel_vm::builtin::BuiltInModule}; + +use crate::{ + // args::Args, + commands::insert, + compositor::{self, Component, Compositor}, + config::Config, + events::{OnModeSwitch, PostCommand, PostInsertChar}, + job::{self, Callback}, + keymap::{self, merge_keys, KeyTrie, KeymapResult}, + ui::{self, picker::PathOrId, PickerColumn, Popup, Prompt, PromptEvent}, +}; + +use components::SteelDynamicComponent; + +use super::{ + components::{self, helix_component_module}, + Context, MappableCommand, TYPABLE_COMMAND_LIST, +}; +use insert::{insert_char, insert_string}; + +pub static INTERRUPT_HANDLER: OnceCell = OnceCell::new(); + +// TODO: Use this for the available commands. +// We just have to look at functions that have been defined at +// the top level, _after_ they +pub static GLOBAL_OFFSET: OnceCell = OnceCell::new(); +// pub static AVAILABLE_FUNCTIONS: Lazy>> = Lazy::new(|| RwLock::new(Vec::new())); + +// The Steel scripting engine instance. This is what drives the whole integration. +pub static GLOBAL_ENGINE: Lazy> = Lazy::new(|| { + let engine = steel::steel_vm::engine::Engine::new(); + + // Any function after this point can be used for looking at "new" functions + GLOBAL_OFFSET.set(engine.readable_globals(0).len()).unwrap(); + + let controller = engine.get_thread_state_controller(); + let running = Arc::new(AtomicBool::new(false)); + + fn is_event_available() -> std::io::Result { + crossterm::event::poll(Duration::from_millis(10)) + } + + let controller_clone = controller.clone(); + let running_clone = running.clone(); + + // TODO: Only allow interrupt after a certain amount of time... + // perhaps something like, 500 ms? That way interleaving calls to + // steel functions don't accidentally cause an interrupt. + let thread_handle = std::thread::spawn(move || { + let controller = controller_clone; + let running = running_clone; + + loop { + std::thread::park(); + + while running.load(std::sync::atomic::Ordering::Relaxed) { + if is_event_available().unwrap_or(false) { + let event = crossterm::event::read(); + + if let Ok(Event::Key(crossterm::event::KeyEvent { + code: KeyCode::Char('c'), + modifiers: KeyModifiers::CONTROL, + .. + })) = event + { + controller.interrupt(); + break; + } + } + } + } + }); + + INTERRUPT_HANDLER + .set(InterruptHandler { + controller: controller.clone(), + running: running.clone(), + handle: thread_handle, + }) + .ok(); + + Mutex::new(configure_engine_impl(engine)) +}); + +fn acquire_engine_lock() -> MutexGuard<'static, Engine> { + GLOBAL_ENGINE.lock().unwrap() +} + +/// Run a function with exclusive access to the engine. This only +/// locks the engine that is running on the main thread. +pub fn enter_engine(f: F) -> R +where + F: FnOnce(&mut Engine) -> R, +{ + (f)(&mut acquire_engine_lock()) +} + +pub struct InterruptHandler { + controller: ThreadStateController, + running: Arc, + handle: std::thread::JoinHandle<()>, +} + +pub fn with_interrupt_handler(f: F) -> R +where + F: FnOnce() -> R, +{ + let handler = INTERRUPT_HANDLER.get().unwrap(); + handler + .running + .store(true, std::sync::atomic::Ordering::Relaxed); + + handler.handle.thread().unpark(); + + let res = (f)(); + + handler.controller.resume(); + handler + .running + .store(false, std::sync::atomic::Ordering::Relaxed); + + res +} + +pub struct KeyMapApi { + default_keymap: fn() -> EmbeddedKeyMap, + empty_keymap: fn() -> EmbeddedKeyMap, + string_to_embedded_keymap: fn(String) -> EmbeddedKeyMap, + merge_keybindings: fn(&mut EmbeddedKeyMap, EmbeddedKeyMap), + is_keymap: fn(SteelVal) -> bool, + deep_copy_keymap: fn(EmbeddedKeyMap) -> EmbeddedKeyMap, +} + +impl KeyMapApi { + fn new() -> Self { + KeyMapApi { + default_keymap, + empty_keymap, + string_to_embedded_keymap, + merge_keybindings, + is_keymap, + deep_copy_keymap, + } + } +} + +// Handle buffer and extension specific keybindings in userspace. +pub static BUFFER_OR_EXTENSION_KEYBINDING_MAP: Lazy = + Lazy::new(|| SteelVal::boxed(SteelVal::empty_hashmap())); + +pub static REVERSE_BUFFER_MAP: Lazy = + Lazy::new(|| SteelVal::boxed(SteelVal::empty_hashmap())); + +fn load_component_api(engine: &mut Engine, generate_sources: bool) { + let module = helix_component_module(); + + if generate_sources { + configure_lsp_builtins("component", &module); + } + + engine.register_module(module); +} + +fn load_keymap_api(engine: &mut Engine, api: KeyMapApi, generate_sources: bool) { + let mut module = BuiltInModule::new("helix/core/keymaps"); + + module.register_fn("helix-empty-keymap", api.empty_keymap); + module.register_fn("helix-default-keymap", api.default_keymap); + module.register_fn("helix-merge-keybindings", api.merge_keybindings); + module.register_fn("helix-string->keymap", api.string_to_embedded_keymap); + module.register_fn("keymap?", api.is_keymap); + + module.register_fn("helix-deep-copy-keymap", api.deep_copy_keymap); + + // This should be associated with a corresponding scheme module to wrap this up + module.register_value( + "*buffer-or-extension-keybindings*", + BUFFER_OR_EXTENSION_KEYBINDING_MAP.clone(), + ); + module.register_value("*reverse-buffer-map*", REVERSE_BUFFER_MAP.clone()); + module.register_fn("keymap-update-documentation!", update_documentation); + + if generate_sources { + configure_lsp_builtins("keymap", &module) + } + + engine.register_module(module); +} + +fn load_static_commands(engine: &mut Engine, generate_sources: bool) { + let mut module = BuiltInModule::new("helix/core/static"); + + let mut builtin_static_command_module = if generate_sources { + "(require-builtin helix/core/static as helix.static.)".to_string() + } else { + "".to_string() + }; + + for command in TYPABLE_COMMAND_LIST { + let func = |cx: &mut Context| { + let mut cx = compositor::Context { + editor: cx.editor, + scroll: None, + jobs: cx.jobs, + }; + + (command.fun)(&mut cx, Args::default(), PromptEvent::Validate) + }; + + module.register_fn(command.name, func); + } + + // Register everything in the static command list as well + // These just accept the context, no arguments + for command in MappableCommand::STATIC_COMMAND_LIST { + if let MappableCommand::Static { name, fun, doc } = command { + module.register_fn(name, fun); + + if generate_sources { + let mut docstring = doc + .lines() + .map(|x| { + let mut line = ";;".to_string(); + line.push_str(x); + line.push_str("\n"); + line + }) + .collect::(); + + docstring.pop(); + + builtin_static_command_module.push_str(&format!( + r#" +(provide {}) +;;@doc +{} +(define ({}) + (helix.static.{} *helix.cx*)) +"#, + name, docstring, name, name + )); + } + } + } + + let mut template_function_arity_1 = |name: &str, doc: &str| { + if generate_sources { + let mut docstring = doc + .lines() + .map(|x| { + let mut line = ";;".to_string(); + line.push_str(x); + line.push_str("\n"); + line + }) + .collect::(); + + docstring.pop(); + + builtin_static_command_module.push_str(&format!( + r#" +(provide {}) +;;@doc +{} +(define ({} arg) + (helix.static.{} *helix.cx* arg)) +"#, + name, docstring, name, name + )); + } + }; + + macro_rules! function1 { + ($name:expr, $function:expr, $doc:expr) => {{ + module.register_fn($name, $function); + template_function_arity_1($name, $doc); + }}; + } + + // Adhoc static commands that probably needs evaluating + // Arity 1 + function1!( + "insert_char", + insert_char, + "Insert a given character at the cursor cursor position" + ); + function1!( + "insert_string", + insert_string, + "Insert a given string at the current cursor position" + ); + + function1!( + "set-current-selection-object!", + set_selection, + "Update the selection object to the current selection within the editor" + ); + + function1!( + "regex-selection", + regex_selection, + "Run the given regex within the existing buffer" + ); + + function1!( + "replace-selection-with", + replace_selection, + "Replace the existing selection with the given string" + ); + + function1!( + "cx->current-file", + current_path, + "Get the currently focused file path" + ); + + function1!( + "enqueue-expression-in-engine", + run_expression_in_engine, + "Enqueue an expression to run at the top level context, + after the existing function context has exited." + ); + + let mut template_function_arity_0 = |name: &str| { + if generate_sources { + builtin_static_command_module.push_str(&format!( + r#" +(provide {}) +(define ({}) + (helix.static.{} *helix.cx*)) +"#, + name, name, name + )); + } + }; + + macro_rules! function0 { + ($name:expr, $function:expr) => {{ + module.register_fn($name, $function); + template_function_arity_0($name); + }}; + } + + function0!("current_selection", get_selection); + function0!("load-buffer!", load_buffer); + function0!("current-highlighted-text!", get_highlighted_text); + function0!("get-current-line-number", current_line_number); + function0!("current-selection-object", current_selection); + function0!("get-helix-cwd", get_helix_cwd); + function0!("move-window-far-left", move_window_to_the_left); + function0!("move-window-far-right", move_window_to_the_right); + + let mut template_function_no_context = |name: &str| { + if generate_sources { + builtin_static_command_module.push_str(&format!( + r#" +(provide {}) +(define {} helix.static.{}) + "#, + name, name, name + )) + } + }; + + module.register_fn("get-helix-scm-path", get_helix_scm_path); + module.register_fn("get-init-scm-path", get_init_scm_path); + + template_function_no_context("get-helix-scm-path"); + template_function_no_context("get-init-scm-path"); + + if generate_sources { + let mut target_directory = helix_runtime_search_path(); + + if !target_directory.exists() { + std::fs::create_dir(&target_directory).unwrap(); + } + + target_directory.push("static.scm"); + + std::fs::write(target_directory, builtin_static_command_module).unwrap(); + } + + if generate_sources { + configure_lsp_builtins("static", &module); + } + + engine.register_module(module); +} + +fn load_typed_commands(engine: &mut Engine, generate_sources: bool) { + let mut module = BuiltInModule::new("helix/core/typable".to_string()); + + let mut builtin_typable_command_module = if generate_sources { + "(require-builtin helix/core/typable as helix.)".to_string() + } else { + "".to_string() + }; + + // Register everything in the typable command list. Now these are all available + for command in TYPABLE_COMMAND_LIST { + // TODO: This needs to get updated + let func = |cx: &mut Context, args: &[Cow]| { + let mut cx = compositor::Context { + editor: cx.editor, + scroll: None, + jobs: cx.jobs, + }; + + (command.fun)(&mut cx, Args::raw(args.to_vec()), PromptEvent::Validate) + }; + + module.register_fn(command.name, func); + + if generate_sources { + // Create an ephemeral builtin module to reference until I figure out how + // to wrap the functions with a reference to the engine context better. + builtin_typable_command_module.push_str(&format!( + r#" +(provide {}) + +;;@doc +{} +(define ({} . args) + (helix.{} *helix.cx* args)) +"#, + command.name, + { + // Ugly hack to drop the extra newline from + // the docstring + let mut docstring = command + .doc + .lines() + .map(|x| { + let mut line = ";;".to_string(); + line.push_str(x); + line.push_str("\n"); + line + }) + .collect::(); + + docstring.pop(); + + docstring + }, + command.name, + command.name + )); + } + } + + if generate_sources { + let mut target_directory = helix_runtime_search_path(); + if !target_directory.exists() { + std::fs::create_dir(&target_directory).unwrap(); + } + + target_directory.push("commands.scm"); + + std::fs::write(target_directory, builtin_typable_command_module).unwrap(); + } + + if generate_sources { + configure_lsp_builtins("typed", &module); + } + + engine.register_module(module); +} + +fn get_option_value(cx: &mut Context, option: String) -> anyhow::Result { + let key_error = || anyhow::anyhow!("Unknown key `{}`", option); + + let config = serde_json::json!(std::ops::Deref::deref(&cx.editor.config())); + let pointer = format!("/{}", option.replace('.', "/")); + let value = config.pointer(&pointer).ok_or_else(key_error)?; + Ok(value.to_owned().into_steelval().unwrap()) +} + +// File picker configurations +fn fp_hidden(config: &mut FilePickerConfig, option: bool) { + config.hidden = option; +} + +fn fp_follow_symlinks(config: &mut FilePickerConfig, option: bool) { + config.follow_symlinks = option; +} + +fn fp_deduplicate_links(config: &mut FilePickerConfig, option: bool) { + config.deduplicate_links = option; +} + +fn fp_parents(config: &mut FilePickerConfig, option: bool) { + config.parents = option; +} + +fn fp_ignore(config: &mut FilePickerConfig, option: bool) { + config.ignore = option; +} + +fn fp_git_ignore(config: &mut FilePickerConfig, option: bool) { + config.git_ignore = option; +} + +fn fp_git_global(config: &mut FilePickerConfig, option: bool) { + config.git_global = option; +} + +fn fp_git_exclude(config: &mut FilePickerConfig, option: bool) { + config.git_exclude = option; +} + +fn fp_max_depth(config: &mut FilePickerConfig, option: Option) { + config.max_depth = option; +} + +// Soft wrap configurations +fn sw_enable(config: &mut SoftWrap, option: Option) { + config.enable = option; +} + +fn sw_max_wrap(config: &mut SoftWrap, option: Option) { + config.max_wrap = option; +} + +fn sw_max_indent_retain(config: &mut SoftWrap, option: Option) { + config.max_indent_retain = option; +} + +fn sw_wrap_indicator(config: &mut SoftWrap, option: Option) { + config.wrap_indicator = option; +} + +fn wrap_at_text_width(config: &mut SoftWrap, option: Option) { + config.wrap_at_text_width = option; +} + +fn load_configuration_api(engine: &mut Engine, generate_sources: bool) { + let mut module = BuiltInModule::new("helix/core/configuration"); + + module.register_fn("update-configuration!", |ctx: &mut Context| { + ctx.editor + .config_events + .0 + .send(ConfigEvent::Change) + .unwrap(); + }); + + module.register_fn("get-config-option-value", get_option_value); + + module.register_fn("set-configuration-for-file!", set_configuration_for_file); + + module + .register_fn( + "get-language-config", + HelixConfiguration::get_language_config, + ) + .register_fn( + "get-language-config-by-filename", + HelixConfiguration::get_individual_language_config_for_filename, + ) + .register_fn( + "set-language-config!", + HelixConfiguration::update_individual_language_config, + ); + + module + .register_fn("raw-file-picker", || FilePickerConfig::default()) + .register_fn("register-file-picker", HelixConfiguration::file_picker) + .register_fn("fp-hidden", fp_hidden) + .register_fn("fp-follow-symlinks", fp_follow_symlinks) + .register_fn("fp-deduplicate-links", fp_deduplicate_links) + .register_fn("fp-parents", fp_parents) + .register_fn("fp-ignore", fp_ignore) + .register_fn("fp-git-ignore", fp_git_ignore) + .register_fn("fp-git-global", fp_git_global) + .register_fn("fp-git-exclude", fp_git_exclude) + .register_fn("fp-max-depth", fp_max_depth); + + module + .register_fn("raw-soft-wrap", || SoftWrap::default()) + .register_fn("register-soft-wrap", HelixConfiguration::soft_wrap) + .register_fn("sw-enable", sw_enable) + .register_fn("sw-max-wrap", sw_max_wrap) + .register_fn("sw-max-indent-retain", sw_max_indent_retain) + .register_fn("sw-wrap-indicator", sw_wrap_indicator) + .register_fn("sw-wrap-at-text-width", wrap_at_text_width); + + module + .register_fn("scrolloff", HelixConfiguration::scrolloff) + .register_fn("scroll_lines", HelixConfiguration::scroll_lines) + .register_fn("mouse", HelixConfiguration::mouse) + .register_fn("shell", HelixConfiguration::shell) + .register_fn("line-number", HelixConfiguration::line_number) + .register_fn("cursorline", HelixConfiguration::cursorline) + .register_fn("cursorcolumn", HelixConfiguration::cursorcolumn) + .register_fn("middle-click-paste", HelixConfiguration::middle_click_paste) + .register_fn("auto-pairs", HelixConfiguration::auto_pairs) + // Specific constructors for the auto pairs configuration + .register_fn("auto-pairs-default", |enabled: bool| { + AutoPairConfig::Enable(enabled) + }) + .register_fn("auto-pairs-map", |map: HashMap| { + AutoPairConfig::Pairs(map) + }) + // TODO: Finish this up + .register_fn("auto-save-default", || AutoSave::default()) + .register_fn( + "auto-save-after-delay-enable", + HelixConfiguration::auto_save_after_delay_enable, + ) + .register_fn( + "inline-diagnostics-cursor-line-enable", + HelixConfiguration::inline_diagnostics_cursor_line_enable, + ) + .register_fn( + "inline-diagnostics-end-of-line-enable", + HelixConfiguration::inline_diagnostics_end_of_line_enable, + ) + .register_fn("auto-completion", HelixConfiguration::auto_completion) + .register_fn("auto-format", HelixConfiguration::auto_format) + .register_fn("auto-save", HelixConfiguration::auto_save) + .register_fn("text-width", HelixConfiguration::text_width) + .register_fn("idle-timeout", HelixConfiguration::idle_timeout) + .register_fn("completion-timeout", HelixConfiguration::completion_timeout) + .register_fn( + "preview-completion-insert", + HelixConfiguration::preview_completion_insert, + ) + .register_fn( + "completion-trigger-len", + HelixConfiguration::completion_trigger_len, + ) + .register_fn("completion-replace", HelixConfiguration::completion_replace) + .register_fn("auto-info", HelixConfiguration::auto_info) + .register_fn("cursor-shape", HelixConfiguration::cursor_shape) + .register_fn("true-color", HelixConfiguration::true_color) + .register_fn( + "insert-final-newline", + HelixConfiguration::insert_final_newline, + ) + .register_fn("color-modes", HelixConfiguration::color_modes) + .register_fn("gutters", HelixConfiguration::gutters) + // .register_fn("file-picker", HelixConfiguration::file_picker) + .register_fn("statusline", HelixConfiguration::statusline) + .register_fn("undercurl", HelixConfiguration::undercurl) + .register_fn("search", HelixConfiguration::search) + .register_fn("lsp", HelixConfiguration::lsp) + .register_fn("terminal", HelixConfiguration::terminal) + .register_fn("rulers", HelixConfiguration::rulers) + .register_fn("whitespace", HelixConfiguration::whitespace) + .register_fn("bufferline", HelixConfiguration::bufferline) + .register_fn("indent-guides", HelixConfiguration::indent_guides) + .register_fn("soft-wrap", HelixConfiguration::soft_wrap) + .register_fn( + "workspace-lsp-roots", + HelixConfiguration::workspace_lsp_roots, + ) + .register_fn( + "default-line-ending", + HelixConfiguration::default_line_ending, + ) + .register_fn("smart-tab", HelixConfiguration::smart_tab); + + // Keybinding stuff + module + .register_fn("keybindings", HelixConfiguration::keybindings) + .register_fn("get-keybindings", HelixConfiguration::get_keybindings); + + if generate_sources { + let mut builtin_configuration_module = + "(require-builtin helix/core/configuration as helix.)".to_string(); + + builtin_configuration_module.push_str(&format!( + r#" +(provide update-configuration!) +(define (update-configuration!) + (helix.update-configuration! *helix.config*)) +"#, + )); + + builtin_configuration_module.push_str(&format!( + r#" +(provide get-config-option-value) +(define (get-config-option-value arg) + (helix.get-config-option-value *helix.cx* arg)) +"#, + )); + + builtin_configuration_module.push_str(&format!( + r#" +(provide set-configuration-for-file!) +(define (set-configuration-for-file! path config) + (helix.set-configuration-for-file! *helix.cx* path config)) +"#, + )); + + // Register the get keybindings function + builtin_configuration_module.push_str(&format!( + r#" +(provide get-keybindings) +(define (get-keybindings) + (helix.get-keybindings *helix.config*)) +"#, + )); + + let mut template_soft_wrap = |name: &str| { + builtin_configuration_module.push_str(&format!( + r#" +(provide {}) +(define ({} arg) + (lambda (picker) + (helix.{} picker arg) + picker)) +"#, + name, name, name + )); + }; + + let soft_wrap_functions = &[ + "sw-enable", + "sw-max-wrap", + "sw-max-indent-retain", + "sw-wrap-indicator", + "sw-wrap-at-text-width", + ]; + + for name in soft_wrap_functions { + template_soft_wrap(name); + } + + let mut template_file_picker_function = |name: &str| { + builtin_configuration_module.push_str(&format!( + r#" +(provide {}) +(define ({} arg) + (lambda (picker) + (helix.{} picker arg) + picker)) +"#, + name, name, name + )); + }; + + let file_picker_functions = &[ + "fp-hidden", + "fp-follow-symlinks", + "fp-deduplicate-links", + "fp-parents", + "fp-ignore", + "fp-git-ignore", + "fp-git-global", + "fp-git-exclude", + "fp-max-depth", + ]; + + for name in file_picker_functions { + template_file_picker_function(name); + } + + builtin_configuration_module.push_str(&format!( + r#" +(provide file-picker) +(define (file-picker . args) + (helix.register-file-picker + *helix.config* + (foldl (lambda (func config) (func config)) (helix.raw-file-picker) args))) +"#, + )); + + builtin_configuration_module.push_str(&format!( + r#" +(provide soft-wrap) +(define (soft-wrap . args) + (helix.register-soft-wrap + *helix.config* + (foldl (lambda (func config) (func config)) (helix.raw-soft-wrap) args))) +"#, + )); + + let mut template_function_arity_1 = |name: &str| { + builtin_configuration_module.push_str(&format!( + r#" +(provide {}) +(define ({} arg) + (helix.{} *helix.config* arg)) +"#, + name, name, name + )); + }; + + let functions = &[ + "scrolloff", + "scroll_lines", + "mouse", + "shell", + "line-number", + "cursorline", + "cursorcolumn", + "middle-click-paste", + "auto-pairs", + "auto-completion", + "auto-format", + "auto-save", + "text-width", + "idle-timeout", + "completion-timeout", + "preview-completion-insert", + "completion-trigger-len", + "completion-replace", + "auto-info", + "cursor-shape", + "true-color", + "insert-final-newline", + "color-modes", + "gutters", + "statusline", + "undercurl", + "search", + "lsp", + "terminal", + "rulers", + "whitespace", + "bufferline", + "indent-guides", + "workspace-lsp-roots", + "default-line-ending", + "smart-tab", + "keybindings", + "inline-diagnostics-cursor-line-enable", + "inline-diagnostics-end-of-line-enable", + // language configuration functions + "get-language-config", + "get-language-config-by-filename", + "set-language-config!", + ]; + + for func in functions { + template_function_arity_1(func); + } + + let mut target_directory = helix_runtime_search_path(); + + if !target_directory.exists() { + std::fs::create_dir(&target_directory).unwrap(); + } + + target_directory.push("configuration.scm"); + + std::fs::write(target_directory, builtin_configuration_module).unwrap(); + } + + if generate_sources { + configure_lsp_builtins("configuration", &module); + } + + engine.register_module(module); +} + +fn languages_api(engine: &mut Engine, generate_sources: bool) { + // TODO: Just look at the `cx.editor.syn_loader` for how to + // manipulate the languages bindings + todo!() +} + +// TODO: +// This isn't the best API since it pretty much requires deserializing +// the whole theme model each time. While its not _horrible_, it is +// certainly not as efficient as it could be. If we could just edit +// the loaded theme in memory already, then it would be a bit nicer. +fn load_theme_api(engine: &mut Engine, generate_sources: bool) { + let mut module = BuiltInModule::new("helix/core/themes"); + module + .register_fn("hashmap->theme", theme_from_json_string) + .register_fn("add-theme!", add_theme) + .register_fn("theme-style", get_style) + .register_fn("theme-set-style!", set_style) + .register_fn("string->color", string_to_color); + + if generate_sources { + configure_lsp_builtins("themes", &module); + } + + engine.register_module(module); +} + +#[derive(Clone)] +struct SteelTheme(Theme); +impl Custom for SteelTheme {} + +fn theme_from_json_string(name: String, value: SteelVal) -> Result { + // TODO: Really don't love this at all. The deserialization should be a bit more elegant + let json_value = serde_json::Value::try_from(value)?; + let value: toml::Value = serde_json::from_str(&serde_json::to_string(&json_value)?)?; + + let (mut theme, _) = Theme::from_toml(value); + theme.set_name(name); + Ok(SteelTheme(theme)) +} + +// Mutate the theme? +fn add_theme(cx: &mut Context, theme: SteelTheme) { + cx.editor + .user_defined_themes + .insert(theme.0.name().to_owned(), theme.0); +} + +fn get_style(theme: &SteelTheme, name: SteelString) -> helix_view::theme::Style { + theme.0.get(name.as_str()).clone() +} + +fn set_style(theme: &mut SteelTheme, name: String, style: helix_view::theme::Style) { + theme.0.set(name, style) +} + +fn string_to_color(string: SteelString) -> Result { + // TODO: Don't expose this directly + helix_view::theme::ThemePalette::string_to_rgb(string.as_str()).map_err(anyhow::Error::msg) +} + +fn current_buffer_area(cx: &mut Context) -> Option { + let focus = cx.editor.tree.focus; + cx.editor.tree.view_id_area(focus) +} + +fn load_editor_api(engine: &mut Engine, generate_sources: bool) { + let mut module = BuiltInModule::new("helix/core/editor"); + + // Types + module.register_fn("Action/Load", || Action::Load); + module.register_fn("Action/Replace", || Action::Replace); + module.register_fn("Action/HorizontalSplit", || Action::HorizontalSplit); + module.register_fn("Action/VerticalSplit", || Action::VerticalSplit); + + // Arity 0 + module.register_fn("editor-focus", cx_current_focus); + module.register_fn("editor-mode", cx_get_mode); + module.register_fn("cx->themes", get_themes); + module.register_fn("editor-all-documents", cx_editor_all_documents); + module.register_fn("cx->cursor", |cx: &mut Context| cx.editor.cursor()); + + // Arity 1 + module.register_fn("editor->doc-id", cx_get_document_id); + module.register_fn("editor-switch!", cx_switch); + module.register_fn("editor-set-focus!", |cx: &mut Context, view_id: ViewId| { + cx.editor.focus(view_id) + }); + module.register_fn("editor-set-mode!", cx_set_mode); + module.register_fn("editor-doc-in-view?", cx_is_document_in_view); + module.register_fn("set-scratch-buffer-name!", set_scratch_buffer_name); + module.register_fn("editor-doc-exists?", cx_document_exists); + + // Arity 2 + module.register_fn("editor-switch-action!", cx_switch_action); + + // Arity 1 + module.register_fn("editor->text", document_id_to_text); + module.register_fn("editor-document->path", document_path); + + module.register_fn("set-editor-clip-right!", |cx: &mut Context, right: u16| { + cx.editor.editor_clipping.right = Some(right); + }); + module.register_fn("set-editor-clip-left!", |cx: &mut Context, left: u16| { + cx.editor.editor_clipping.left = Some(left); + }); + module.register_fn("set-editor-clip-top!", |cx: &mut Context, top: u16| { + cx.editor.editor_clipping.top = Some(top); + }); + module.register_fn( + "set-editor-clip-bottom!", + |cx: &mut Context, bottom: u16| { + cx.editor.editor_clipping.bottom = Some(bottom); + }, + ); + + module.register_fn("editor-focused-buffer-area", current_buffer_area); + + if generate_sources { + let mut builtin_editor_command_module = + "(require-builtin helix/core/editor as helix.)".to_string(); + + let mut template_function_type_constructor = |name: &str| { + builtin_editor_command_module.push_str(&format!( + r#" +(provide {}) +(define ({}) + (helix.{})) +"#, + name, name, name + )); + }; + + template_function_type_constructor("Action/Load"); + template_function_type_constructor("Action/Replace"); + template_function_type_constructor("Action/HorizontalSplit"); + template_function_type_constructor("Action/VerticalSplit"); + + let mut template_function_arity_0 = |name: &str| { + builtin_editor_command_module.push_str(&format!( + r#" +(provide {}) +(define ({}) + (helix.{} *helix.cx*)) +"#, + name, name, name + )); + }; + + template_function_arity_0("editor-focus"); + template_function_arity_0("editor-mode"); + template_function_arity_0("cx->themes"); + template_function_arity_0("editor-all-documents"); + template_function_arity_0("cx->cursor"); + template_function_arity_0("editor-focused-buffer-area"); + + let mut template_function_arity_1 = |name: &str| { + builtin_editor_command_module.push_str(&format!( + r#" +(provide {}) +(define ({} arg) + (helix.{} *helix.cx* arg)) +"#, + name, name, name + )); + }; + + template_function_arity_1("editor->doc-id"); + template_function_arity_1("editor-switch!"); + template_function_arity_1("editor-set-focus!"); + template_function_arity_1("editor-set-mode!"); + template_function_arity_1("editor-doc-in-view?"); + template_function_arity_1("set-scratch-buffer-name!"); + template_function_arity_1("editor-doc-exists?"); + template_function_arity_1("editor->text"); + template_function_arity_1("editor-document->path"); + + template_function_arity_1("set-editor-clip-top!"); + template_function_arity_1("set-editor-clip-right!"); + template_function_arity_1("set-editor-clip-left!"); + template_function_arity_1("set-editor-clip-bottom!"); + + let mut template_function_arity_2 = |name: &str| { + builtin_editor_command_module.push_str(&format!( + r#" +(provide {}) +(define ({} arg1 arg2) + (helix.{} *helix.cx* arg1 arg2)) +"#, + name, name, name + )); + }; + + template_function_arity_2("editor-switch-action!"); + + let mut target_directory = helix_runtime_search_path(); + + if !target_directory.exists() { + std::fs::create_dir_all(&target_directory).unwrap_or_else(|err| { + panic!("Failed to create directory {:?}: {}", target_directory, err) + }); + eprintln!("Created directory: {:?}", target_directory); + } + + target_directory.push("editor.scm"); + + std::fs::write(target_directory, builtin_editor_command_module).unwrap(); + } + + // Generate the lsp configuration + if generate_sources { + configure_lsp_builtins("editor", &module); + } + + engine.register_module(module); +} + +pub struct SteelScriptingEngine; + +impl super::PluginSystem for SteelScriptingEngine { + fn initialize(&self) { + initialize_engine(); + } + + fn engine_name(&self) -> super::PluginSystemKind { + super::PluginSystemKind::Steel + } + + fn run_initialization_script( + &self, + cx: &mut Context, + configuration: Arc>>, + // Just apply... all the configurations at once? + language_configuration: Arc>, + ) { + run_initialization_script(cx, configuration, language_configuration); + } + + fn handle_keymap_event( + &self, + editor: &mut ui::EditorView, + mode: Mode, + cxt: &mut Context, + event: KeyEvent, + ) -> Option { + SteelScriptingEngine::get_keymap_for_extension(cxt).and_then(|map| { + if let steel::SteelVal::Custom(inner) = map { + if let Some(underlying) = + steel::rvals::as_underlying_type::(inner.read().as_ref()) + { + return Some(editor.keymaps.get_with_map(&underlying.0, mode, event)); + } + } + + None + }) + } + + fn call_function_by_name(&self, cx: &mut Context, name: &str, args: &[Cow]) -> bool { + if enter_engine(|x| x.global_exists(name)) { + let args = args + .iter() + .map(|x| x.clone().into_steelval().unwrap()) + .collect::>(); + + if let Err(e) = enter_engine(|guard| { + { + // Install the interrupt handler, in the event this thing + // is blocking for too long. + with_interrupt_handler(|| { + guard.with_mut_reference::(cx).consume( + move |engine, arguments| { + let context = arguments[0].clone(); + engine.update_value("*helix.cx*", context); + + // TODO: Get rid of this clone + engine.call_function_by_name_with_args(name, args.clone()) + }, + ) + }) + } + }) { + cx.editor.set_error(format!("{}", e)); + } + true + } else { + false + } + } + + fn call_typed_command<'a>( + &self, + cx: &mut compositor::Context, + command: &'a str, + parts: &'a [&'a str], + event: PromptEvent, + ) -> bool { + if enter_engine(|x| x.global_exists(command)) { + let args = parts; + + // We're finalizing the event - we actually want to call the function + if event == PromptEvent::Validate { + if let Err(e) = enter_engine(|guard| { + let args = args + .iter() + .map(|x| x.into_steelval().unwrap()) + .collect::>(); + + let res = { + let mut ctx = Context { + register: None, + count: std::num::NonZeroUsize::new(1), + editor: cx.editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs: cx.jobs, + }; + + // Install interrupt handler here during the duration + // of the function call + match with_interrupt_handler(|| { + guard + .with_mut_reference(&mut ctx) + .consume(move |engine, arguments| { + let context = arguments[0].clone(); + engine.update_value("*helix.cx*", context); + // TODO: Fix this clone + engine.call_function_by_name_with_args(command, args.clone()) + }) + }) { + Ok(res) => { + cx.editor.set_status(res.to_string()); + Ok(res) + } + Err(e) => Err(e), + } + }; + + res + }) { + let mut ctx = Context { + register: None, + count: None, + editor: &mut cx.editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs: &mut cx.jobs, + }; + + enter_engine(|x| present_error_inside_engine_context(&mut ctx, x, e)); + }; + } + + // Global exists + true + } else { + // Global does not exist + false + } + } + + fn get_doc_for_identifier(&self, ident: &str) -> Option { + enter_engine(|engine| get_doc_for_global(engine, ident)) + } + + // Just dump docs for all top level values? + fn available_commands<'a>(&self) -> Vec> { + enter_engine(|engine| { + engine + .readable_globals(*GLOBAL_OFFSET.get().unwrap()) + .iter() + .map(|x| x.resolve().to_string().into()) + .collect() + }) + } + + fn generate_sources(&self) { + // Generate sources directly with a fresh engine + let mut engine = Engine::new(); + configure_builtin_sources(&mut engine, true); + // Generate documentation as well + let target = helix_runtime_search_path(); + + let mut writer = std::io::BufWriter::new(std::fs::File::create("steel-docs.md").unwrap()); + + // Generate markdown docs + steel_doc::walk_dir(&mut writer, target, &mut engine).unwrap(); + } +} + +impl SteelScriptingEngine { + // Attempt to fetch the keymap for the extension + fn get_keymap_for_extension<'a>(cx: &'a mut Context) -> Option { + // Get the currently activated extension, also need to check the + // buffer type. + let extension = { + let current_focus = cx.editor.tree.focus; + let view = cx.editor.tree.get(current_focus); + let doc = &view.doc; + let current_doc = cx.editor.documents.get(doc); + + current_doc + .and_then(|x| x.path()) + .and_then(|x| x.extension()) + .and_then(|x| x.to_str()) + }; + + let doc_id = { + let current_focus = cx.editor.tree.focus; + let view = cx.editor.tree.get(current_focus); + let doc = &view.doc; + + doc + }; + + if let Some(extension) = extension { + if let SteelVal::Boxed(boxed_map) = BUFFER_OR_EXTENSION_KEYBINDING_MAP.clone() { + if let SteelVal::HashMapV(map) = boxed_map.read().clone() { + if let Some(value) = map.get(&SteelVal::StringV(extension.into())) { + if let SteelVal::Custom(inner) = value { + if let Some(_) = steel::rvals::as_underlying_type::( + inner.read().as_ref(), + ) { + return Some(value.clone()); + } + } + } + } + } + } + + if let SteelVal::Boxed(boxed_map) = REVERSE_BUFFER_MAP.clone() { + if let SteelVal::HashMapV(map) = boxed_map.read().clone() { + if let Some(label) = map.get(&SteelVal::IntV(document_id_to_usize(doc_id) as isize)) + { + if let SteelVal::Boxed(boxed_map) = BUFFER_OR_EXTENSION_KEYBINDING_MAP.clone() { + if let SteelVal::HashMapV(map) = boxed_map.read().clone() { + if let Some(value) = map.get(label) { + if let SteelVal::Custom(inner) = value { + if let Some(_) = + steel::rvals::as_underlying_type::( + inner.read().as_ref(), + ) + { + return Some(value.clone()); + } + } + } + } + } + } + } + } + + None + } +} + +pub fn initialize_engine() { + enter_engine(|x| x.globals().first().copied()); +} + +pub fn present_error_inside_engine_context(cx: &mut Context, engine: &mut Engine, e: SteelErr) { + cx.editor.set_error(e.to_string()); + + let backtrace = engine.raise_error_to_string(e); + + let callback = async move { + let call: job::Callback = Callback::EditorCompositor(Box::new( + move |editor: &mut Editor, compositor: &mut Compositor| { + if let Some(backtrace) = backtrace { + let contents = ui::Markdown::new( + format!("```\n{}\n```", backtrace), + editor.syn_loader.clone(), + ); + let popup = Popup::new("engine", contents).position(Some( + helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2), + )); + compositor.replace_or_push("engine", popup); + } + }, + )); + Ok(call) + }; + cx.jobs.callback(callback); +} + +// Key maps +#[derive(Clone, Debug)] +pub struct EmbeddedKeyMap(pub HashMap); +impl Custom for EmbeddedKeyMap {} + +pub fn update_documentation(map: &mut EmbeddedKeyMap, docs: HashMap) { + let mut func = move |command: &mut MappableCommand| { + if let Some(steel_doc) = docs.get(command.name()) { + if let Some(doc) = command.doc_mut() { + *doc = steel_doc.to_owned() + } + } + }; + + for trie in map.0.values_mut() { + trie.apply(&mut func) + } +} + +// Will deep copy a value by default when using a value type +pub fn deep_copy_keymap(copied: EmbeddedKeyMap) -> EmbeddedKeyMap { + copied +} + +// Base level - no configuration +pub fn default_keymap() -> EmbeddedKeyMap { + EmbeddedKeyMap(keymap::default()) +} + +// Completely empty, allow for overriding +pub fn empty_keymap() -> EmbeddedKeyMap { + EmbeddedKeyMap(HashMap::default()) +} + +pub fn string_to_embedded_keymap(value: String) -> EmbeddedKeyMap { + EmbeddedKeyMap(serde_json::from_str(&value).unwrap()) +} + +pub fn merge_keybindings(left: &mut EmbeddedKeyMap, right: EmbeddedKeyMap) { + merge_keys(&mut left.0, right.0) +} + +pub fn is_keymap(keymap: SteelVal) -> bool { + if let SteelVal::Custom(underlying) = keymap { + as_underlying_type::(underlying.read().as_ref()).is_some() + } else { + false + } +} + +fn local_config_exists() -> bool { + let local_helix = find_workspace().0.join(".helix"); + local_helix.join("helix.scm").exists() && local_helix.join("init.scm").exists() +} + +fn preferred_config_path(file_name: &str) -> PathBuf { + if local_config_exists() { + find_workspace().0.join(".helix").join(file_name) + } else { + helix_loader::config_dir().join(file_name) + } +} + +pub fn helix_module_file() -> PathBuf { + preferred_config_path("helix.scm") +} + +pub fn steel_init_file() -> PathBuf { + preferred_config_path("init.scm") +} + +struct HelixConfiguration { + configuration: Arc>>, + language_configuration: Arc>, +} + +#[derive(Clone)] +struct IndividualLanguageConfiguration { + config: LanguageConfiguration, +} + +impl Custom for IndividualLanguageConfiguration {} + +impl IndividualLanguageConfiguration { + pub fn set_indentation_config(&mut self, tab_width: usize, unit: String) { + self.config.indent = Some(IndentationConfiguration { tab_width, unit }); + } + + // Apply end of line configuration on doc open? + // pub fn set_end_of_line(&mut self) { + // self.config.end + // } +} + +impl Custom for HelixConfiguration {} + +// Set the configuration for an individual file. +fn update_configuration_for_file(ctx: &mut Context, doc: DocumentId) { + if let Some(document) = ctx.editor.documents.get_mut(&doc) { + let path = document.path().unwrap(); + let config_for_file = ctx + .editor + .syn_loader + .load() + .language_config_for_file_name(path); + + document.language = config_for_file; + } +} + +fn set_configuration_for_file( + ctx: &mut Context, + file_name: SteelString, + configuration: IndividualLanguageConfiguration, +) { + if let Some(document) = ctx.editor.document_by_path_mut(file_name.as_str()) { + document.language = Some(Arc::new(configuration.config)); + } +} + +impl HelixConfiguration { + fn store_language_configuration(&self, language_config: syntax::Loader) { + self.language_configuration.store(Arc::new(language_config)) + } + + fn get_language_config( + &self, + language: SteelString, + ) -> Option { + self.language_configuration + .load() + .language_config_for_language_id(language.as_str()) + .map(|config| IndividualLanguageConfiguration { + config: (*config).clone(), + }) + } + + fn get_individual_language_config_for_filename( + &self, + file_name: SteelString, + ) -> Option { + self.language_configuration + .load() + .language_config_for_file_name(std::path::Path::new(file_name.as_str())) + .map(|config| IndividualLanguageConfiguration { + config: (*config).clone(), + }) + } + + // Update the language config - this does not immediately flush it + // to the actual config. + fn update_individual_language_config(&mut self, config: IndividualLanguageConfiguration) { + // TODO: Try to opportunistically load the ref counts + // of the inner values - if the documents haven't been opened yet, we + // don't need to clone the _whole_ loader. + let mut loader = (*(*self.language_configuration.load())).clone(); + let config = config.config; + + for lconfig in loader.language_configs_mut() { + if &lconfig.language_id == &config.language_id { + if let Some(inner) = Arc::get_mut(lconfig) { + *inner = config; + } else { + *lconfig = Arc::new(config); + } + break; + } + } + } + + // // Refresh configuration for a specific file + // fn refresh_language_configuration(&mut self) { + // todo!() + // } + + fn load_config(&self) -> Config { + (*self.configuration.load().clone()).clone() + } + + fn store_config(&self, config: Config) { + self.configuration.store(Arc::new(config)); + } + + // Overlay new keybindings + fn keybindings(&self, keybindings: EmbeddedKeyMap) { + let mut app_config = self.load_config(); + merge_keys(&mut app_config.keys, keybindings.0); + self.store_config(app_config); + } + + fn get_keybindings(&self) -> EmbeddedKeyMap { + EmbeddedKeyMap(self.load_config().keys.clone()) + } + + fn scrolloff(&self, lines: usize) { + let mut app_config = self.load_config(); + app_config.editor.scrolloff = lines; + self.store_config(app_config); + } + + fn scroll_lines(&self, lines: isize) { + let mut app_config = self.load_config(); + app_config.editor.scroll_lines = lines; + self.store_config(app_config); + } + + fn mouse(&self, m: bool) { + let mut app_config = self.load_config(); + app_config.editor.mouse = m; + self.store_config(app_config); + } + + fn shell(&self, shell: Vec) { + let mut app_config = self.load_config(); + app_config.editor.shell = shell; + self.store_config(app_config); + } + + // TODO: Make this a symbol, probably! + fn line_number(&self, mode: LineNumber) { + let mut app_config = self.load_config(); + app_config.editor.line_number = mode; + self.store_config(app_config); + } + + fn cursorline(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.cursorline = option; + self.store_config(app_config); + } + + fn cursorcolumn(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.cursorcolumn = option; + self.store_config(app_config); + } + + fn middle_click_paste(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.middle_click_paste = option; + self.store_config(app_config); + } + + fn auto_pairs(&self, config: AutoPairConfig) { + let mut app_config = self.load_config(); + app_config.editor.auto_pairs = config; + self.store_config(app_config); + } + + fn auto_completion(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.auto_completion = option; + self.store_config(app_config); + } + + fn auto_format(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.auto_format = option; + self.store_config(app_config); + } + + fn auto_save(&self, option: AutoSave) { + let mut app_config = self.load_config(); + app_config.editor.auto_save = option; + self.store_config(app_config); + } + + // TODO: Finish the auto save options! + fn auto_save_after_delay_enable(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.auto_save.after_delay.enable = option; + self.store_config(app_config); + } + + // TODO: Finish diagnostic options! + fn inline_diagnostics_cursor_line_enable(&self, severity: String) { + let mut app_config = self.load_config(); + let severity = match severity.as_str() { + "hint" => Severity::Hint, + "info" => Severity::Info, + "warning" => Severity::Warning, + "error" => Severity::Error, + _ => return, + }; + app_config.editor.inline_diagnostics.cursor_line = DiagnosticFilter::Enable(severity); + self.store_config(app_config); + } + + fn inline_diagnostics_end_of_line_enable(&self, severity: String) { + let mut app_config = self.load_config(); + let severity = match severity.as_str() { + "hint" => Severity::Hint, + "info" => Severity::Info, + "warning" => Severity::Warning, + "error" => Severity::Error, + _ => return, + }; + app_config.editor.end_of_line_diagnostics = DiagnosticFilter::Enable(severity); + self.store_config(app_config); + } + + fn text_width(&self, width: usize) { + let mut app_config = self.load_config(); + app_config.editor.text_width = width; + self.store_config(app_config); + } + + fn idle_timeout(&self, ms: usize) { + let mut app_config = self.load_config(); + app_config.editor.idle_timeout = Duration::from_millis(ms as u64); + self.store_config(app_config); + } + + fn completion_timeout(&self, ms: usize) { + let mut app_config = self.load_config(); + app_config.editor.completion_timeout = Duration::from_millis(ms as u64); + self.store_config(app_config); + } + + fn preview_completion_insert(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.preview_completion_insert = option; + self.store_config(app_config); + } + + // TODO: Make sure this conversion works automatically + fn completion_trigger_len(&self, length: u8) { + let mut app_config = self.load_config(); + app_config.editor.completion_trigger_len = length; + self.store_config(app_config); + } + + fn completion_replace(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.completion_replace = option; + self.store_config(app_config); + } + + fn auto_info(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.auto_info = option; + self.store_config(app_config); + } + + fn cursor_shape(&self, config: CursorShapeConfig) { + let mut app_config = self.load_config(); + app_config.editor.cursor_shape = config; + self.store_config(app_config); + } + + fn true_color(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.true_color = option; + self.store_config(app_config); + } + + fn insert_final_newline(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.insert_final_newline = option; + self.store_config(app_config); + } + + fn color_modes(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.color_modes = option; + self.store_config(app_config); + } + + fn gutters(&self, config: GutterConfig) { + let mut app_config = self.load_config(); + app_config.editor.gutters = config; + self.store_config(app_config); + } + + fn file_picker(&self, picker: FilePickerConfig) { + let mut app_config = self.load_config(); + app_config.editor.file_picker = picker; + self.store_config(app_config); + } + + fn statusline(&self, config: StatusLineConfig) { + let mut app_config = self.load_config(); + app_config.editor.statusline = config; + self.store_config(app_config); + } + + fn undercurl(&self, undercurl: bool) { + let mut app_config = self.load_config(); + app_config.editor.undercurl = undercurl; + self.store_config(app_config); + } + + fn search(&self, config: SearchConfig) { + let mut app_config = self.load_config(); + app_config.editor.search = config; + self.store_config(app_config); + } + + fn lsp(&self, config: LspConfig) { + let mut app_config = self.load_config(); + app_config.editor.lsp = config; + self.store_config(app_config); + } + + fn terminal(&self, config: Option) { + let mut app_config = self.load_config(); + app_config.editor.terminal = config; + self.store_config(app_config); + } + + fn rulers(&self, cols: Vec) { + let mut app_config = self.load_config(); + app_config.editor.rulers = cols; + self.store_config(app_config); + } + + fn whitespace(&self, config: WhitespaceConfig) { + let mut app_config = self.load_config(); + app_config.editor.whitespace = config; + self.store_config(app_config); + } + + fn bufferline(&self, config: BufferLine) { + let mut app_config = self.load_config(); + app_config.editor.bufferline = config; + self.store_config(app_config); + } + + fn indent_guides(&self, config: IndentGuidesConfig) { + let mut app_config = self.load_config(); + app_config.editor.indent_guides = config; + self.store_config(app_config); + } + + fn soft_wrap(&self, config: SoftWrap) { + let mut app_config = self.load_config(); + app_config.editor.soft_wrap = config; + self.store_config(app_config); + } + + fn workspace_lsp_roots(&self, roots: Vec) { + let mut app_config = self.load_config(); + app_config.editor.workspace_lsp_roots = roots; + self.store_config(app_config); + } + + fn default_line_ending(&self, config: LineEndingConfig) { + let mut app_config = self.load_config(); + app_config.editor.default_line_ending = config; + self.store_config(app_config); + } + + fn smart_tab(&self, config: Option) { + let mut app_config = self.load_config(); + app_config.editor.smart_tab = config; + self.store_config(app_config); + } +} + +// Get doc from function ptr table, hack +fn get_doc_for_global(engine: &mut Engine, ident: &str) -> Option { + if engine.global_exists(ident) { + let expr = format!("(#%function-ptr-table-get #%function-ptr-table {})", ident); + Some( + engine + .run(expr) + .ok() + .and_then(|x| x.first().cloned()) + .and_then(|x| x.as_string().map(|x| x.as_str().to_string())) + .unwrap_or_else(|| "Undocumented plugin command".to_string()), + ) + } else { + None + } +} + +/// Run the initialization script located at `$helix_config/init.scm` +/// This runs the script in the global environment, and does _not_ load it as a module directly +fn run_initialization_script( + cx: &mut Context, + configuration: Arc>>, + language_configuration: Arc>, +) { + log::info!("Loading init.scm..."); + + let helix_module_path = helix_module_file(); + + // TODO: Report the error from requiring the file! + enter_engine(|guard| { + // Embed the configuration so we don't have to communicate over the refresh + // channel. The state is still stored within the `Application` struct, but + // now we can just access it and signal a refresh of the config when we need to. + guard.update_value( + "*helix.config*", + HelixConfiguration { + configuration, + language_configuration, + } + .into_steelval() + .unwrap(), + ); + + let res = guard.run_with_reference( + cx, + "*helix.cx*", + &format!(r#"(require {:?})"#, helix_module_path.to_str().unwrap()), + ); + + // Present the error in the helix.scm loading + if let Err(e) = res { + present_error_inside_engine_context(cx, guard, e); + return; + } + + let helix_module_path = steel_init_file(); + + // These contents need to be registered with the path? + if let Ok(contents) = std::fs::read_to_string(&helix_module_path) { + let res = guard.run_with_reference_from_path::( + cx, + "*helix.cx*", + &contents, + helix_module_path, + ); + + match res { + Ok(_) => {} + Err(e) => present_error_inside_engine_context(cx, guard, e), + } + + log::info!("Finished loading init.scm!") + } else { + log::info!("No init.scm found, skipping loading.") + } + }); +} + +impl Custom for PromptEvent {} + +impl<'a> CustomReference for Context<'a> {} + +steel::custom_reference!(Context<'a>); + +fn get_themes(cx: &mut Context) -> Vec { + ui::completers::theme(cx.editor, "") + .into_iter() + .map(|x| x.1.content.to_string()) + .collect() +} + +/// A dynamic component, used for rendering thing +impl Custom for compositor::EventResult {} + +pub struct WrappedDynComponent { + pub(crate) inner: Option>, +} + +impl Custom for WrappedDynComponent {} + +pub struct BoxDynComponent { + inner: Box, +} + +impl BoxDynComponent { + pub fn new(inner: Box) -> Self { + Self { inner } + } +} + +impl Component for BoxDynComponent { + fn handle_event( + &mut self, + _event: &helix_view::input::Event, + _ctx: &mut compositor::Context, + ) -> compositor::EventResult { + self.inner.handle_event(_event, _ctx) + } + + fn should_update(&self) -> bool { + self.inner.should_update() + } + + fn cursor( + &self, + _area: helix_view::graphics::Rect, + _ctx: &Editor, + ) -> ( + Option, + helix_view::graphics::CursorKind, + ) { + self.inner.cursor(_area, _ctx) + } + + fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> { + self.inner.required_size(_viewport) + } + + fn type_name(&self) -> &'static str { + std::any::type_name::() + } + + fn id(&self) -> Option<&'static str> { + Some(self.inner.type_name()) + } + + fn name(&self) -> Option<&str> { + self.inner.name() + } + + fn render( + &mut self, + area: helix_view::graphics::Rect, + frame: &mut tui::buffer::Buffer, + ctx: &mut compositor::Context, + ) { + self.inner.render(area, frame, ctx) + } +} + +#[derive(Debug, Clone, Copy)] +struct OnModeSwitchEvent { + old_mode: Mode, + new_mode: Mode, +} + +impl OnModeSwitchEvent { + pub fn get_old_mode(&self) -> Mode { + self.old_mode + } + + pub fn get_new_mode(&self) -> Mode { + self.new_mode + } +} + +impl Custom for OnModeSwitchEvent {} +impl Custom for MappableCommand {} + +// Don't take the function name, just take the function itself? +fn register_hook(event_kind: String, callback_fn: SteelVal) -> steel::UnRecoverableResult { + let rooted = callback_fn.as_rooted(); + + match event_kind.as_str() { + "on-mode-switch" => { + register_hook!(move |event: &mut OnModeSwitch<'_, '_>| { + if let Err(e) = enter_engine(|guard| { + let minimized_event = OnModeSwitchEvent { + old_mode: event.old_mode, + new_mode: event.new_mode, + }; + + guard.with_mut_reference(event.cx).consume(|engine, args| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + let mut args = [minimized_event.into_steelval().unwrap()]; + // engine.call_function_by_name_with_args(&function_name, args) + engine.call_function_with_args_from_mut_slice( + rooted.value().clone(), + &mut args, + ) + }) + }) { + event.cx.editor.set_error(e.to_string()); + } + + Ok(()) + }); + + Ok(SteelVal::Void).into() + } + "post-insert-char" => { + register_hook!(move |event: &mut PostInsertChar<'_, '_>| { + if let Err(e) = enter_engine(|guard| { + guard.with_mut_reference(event.cx).consume(|engine, args| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + let mut args = [event.c.into()]; + engine.call_function_with_args_from_mut_slice( + rooted.value().clone(), + &mut args, + ) + }) + }) { + event.cx.editor.set_error(e.to_string()); + } + + Ok(()) + }); + + Ok(SteelVal::Void).into() + } + // Register hook - on save? + "post-command" => { + register_hook!(move |event: &mut PostCommand<'_, '_>| { + if let Err(e) = enter_engine(|guard| { + guard.with_mut_reference(event.cx).consume(|engine, args| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + let mut args = [event.command.name().into_steelval().unwrap()]; + engine.call_function_with_args_from_mut_slice( + rooted.value().clone(), + &mut args, + ) + }) + }) { + event.cx.editor.set_error(e.to_string()); + } + + Ok(()) + }); + + Ok(SteelVal::Void).into() + } + + "document-focus-lost" => { + // TODO: Pass the information from the event in here - the doc id + // is probably the most helpful so that way we can look the document up + // and act accordingly? + register_hook!(move |event: &mut DocumentFocusLost<'_>| { + let cloned_func = rooted.value().clone(); + let doc_id = event.doc; + + let callback = move |editor: &mut Editor, + _compositor: &mut Compositor, + jobs: &mut job::Jobs| { + let mut ctx = Context { + register: None, + count: None, + editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs, + }; + enter_engine(|guard| { + if let Err(e) = guard + .with_mut_reference::(&mut ctx) + .consume(move |engine, args| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + let mut args = [doc_id.into_steelval().unwrap()]; + + // TODO: Do something with this error! + engine.call_function_with_args_from_mut_slice( + cloned_func.clone(), + &mut args, + ) + }) + { + present_error_inside_engine_context(&mut ctx, guard, e); + } + }) + }; + job::dispatch_blocking_jobs(callback); + + Ok(()) + }); + + Ok(SteelVal::Void).into() + } + + "selection-did-change" => { + // TODO: Pass the information from the event in here - the doc id + // is probably the most helpful so that way we can look the document up + // and act accordingly? + register_hook!(move |event: &mut SelectionDidChange<'_>| { + let cloned_func = rooted.value().clone(); + let view_id = event.view; + + let callback = move |editor: &mut Editor, + _compositor: &mut Compositor, + jobs: &mut job::Jobs| { + let mut ctx = Context { + register: None, + count: None, + editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs, + }; + enter_engine(|guard| { + if let Err(e) = guard + .with_mut_reference::(&mut ctx) + .consume(move |engine, args| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + // TODO: Reuse this allocation + let mut args = [view_id.into_steelval().unwrap()]; + engine.call_function_with_args_from_mut_slice( + cloned_func.clone(), + &mut args, + ) + }) + { + present_error_inside_engine_context(&mut ctx, guard, e); + } + }) + }; + job::dispatch_blocking_jobs(callback); + + Ok(()) + }); + + Ok(SteelVal::Void).into() + } + + "document-opened" => { + // TODO: Share this code with the above since most of it is + // exactly the same + register_hook!(move |event: &mut DocumentDidOpen<'_>| { + let cloned_func = rooted.value().clone(); + let doc_id = event.doc; + + let callback = move |editor: &mut Editor, + _compositor: &mut Compositor, + jobs: &mut job::Jobs| { + let mut ctx = Context { + register: None, + count: None, + editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs, + }; + enter_engine(|guard| { + if let Err(e) = guard + .with_mut_reference::(&mut ctx) + .consume(move |engine, args| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + // TODO: Reuse this allocation if possible + let mut args = [doc_id.into_steelval().unwrap()]; + engine.call_function_with_args_from_mut_slice( + cloned_func.clone(), + &mut args, + ) + }) + { + present_error_inside_engine_context(&mut ctx, guard, e); + } + }) + }; + job::dispatch_blocking_jobs(callback); + + Ok(()) + }); + + Ok(SteelVal::Void).into() + } + + _ => steelerr!(Generic => "Unable to register hook: Unknown event type: {}", event_kind) + .into(), + } +} + +fn configure_lsp_globals() { + if let Ok(steel_lsp_home) = std::env::var("STEEL_LSP_HOME") { + let mut path = PathBuf::from(steel_lsp_home); + path.push("_helix-global-builtins.scm"); + + let mut output = String::new(); + + let names = &[ + "*helix.cx*", + "*helix.config*", + "*helix.id*", + "register-hook!", + "log::info!", + "fuzzy-match", + "helix-find-workspace", + "doc-id->usize", + "new-component!", + "acquire-context-lock", + "SteelDynamicComponent?", + "prompt", + "picker", + "Component::Text", + "hx.create-directory", + ]; + + for value in names { + use std::fmt::Write; + writeln!(&mut output, "(#%register-global '{})", value).unwrap(); + } + + std::fs::write(path, output).unwrap(); + } +} + +fn configure_lsp_builtins(name: &str, module: &BuiltInModule) { + if let Ok(steel_lsp_home) = std::env::var("STEEL_LSP_HOME") { + let mut path = PathBuf::from(steel_lsp_home); + path.push(&format!("_helix-{}-builtins.scm", name)); + + let mut output = String::new(); + + output.push_str(&format!( + r#"(define #%helix-{}-module (#%module "{}")) + +(define (register-values module values) + (map (lambda (ident) (#%module-add module (symbol->string ident) void)) values)) +"#, + name, + module.name() + )); + + output.push_str(&format!(r#"(register-values #%helix-{}-module '("#, name)); + + for value in module.names() { + use std::fmt::Write; + writeln!(&mut output, "{}", value).unwrap(); + } + + output.push_str("))"); + + std::fs::write(path, output).unwrap(); + } +} + +fn load_rope_api(engine: &mut Engine, generate_sources: bool) { + // Wrap the rope module? + let rope_slice_module = rope_module(); + + if generate_sources { + configure_lsp_builtins("rope", &rope_slice_module); + } + + engine.register_module(rope_slice_module); +} + +// struct SteelEngine(Engine); + +// impl SteelEngine { +// pub fn call_function_by_name( +// &mut self, +// function_name: SteelString, +// args: Vec, +// ) -> steel::rvals::Result { +// self.0 +// .call_function_by_name_with_args(function_name.as_str(), args.into_iter().collect()) +// } + +// /// Calling a function that was not defined in the runtime it was created in could +// /// result in panics. You have been warned. +// pub fn call_function( +// &mut self, +// function: SteelVal, +// args: Vec, +// ) -> steel::rvals::Result { +// self.0 +// .call_function_with_args(function, args.into_iter().collect()) +// } + +// pub fn require_module(&mut self, module: SteelString) -> steel::rvals::Result<()> { +// self.0.run(format!("(require \"{}\")", module)).map(|_| ()) +// } +// } + +// impl Custom for SteelEngine {} + +// static ENGINE_ID: AtomicUsize = AtomicUsize::new(0); + +// thread_local! { +// pub static ENGINE_MAP: SteelVal = +// SteelVal::boxed(SteelVal::empty_hashmap()); +// } + +// Low level API work, these need to be loaded into the global environment in a predictable +// location, otherwise callbacks from plugin engines will not be handled properly! +// fn load_engine_api(engine: &mut Engine) { +// fn id_to_engine(value: SteelVal) -> Option { +// if let SteelVal::Boxed(b) = ENGINE_MAP.with(|x| x.clone()) { +// if let SteelVal::HashMapV(h) = b.read().clone() { +// return h.get(&value).cloned(); +// } +// } + +// None +// } + +// // module +// engine +// .register_fn("helix.controller.create-engine", || { +// SteelEngine(configure_engine_impl(Engine::new())) +// }) +// .register_fn("helix.controller.fresh-engine-id", || { +// ENGINE_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst) +// }) +// .register_fn( +// "helix.controller.call-function-by-name", +// SteelEngine::call_function_by_name, +// ) +// .register_fn("helix.controller.call-function", SteelEngine::call_function) +// .register_fn( +// "helix.controller.require-module", +// SteelEngine::require_module, +// ) +// .register_value( +// "helix.controller.engine-map", +// ENGINE_MAP.with(|x| x.clone()), +// ) +// .register_fn("helix.controller.id->engine", id_to_engine); +// } + +fn load_misc_api(engine: &mut Engine, generate_sources: bool) { + let mut module = BuiltInModule::new("helix/core/misc"); + + let mut builtin_misc_module = if generate_sources { + "(require-builtin helix/core/misc as helix.)".to_string() + } else { + "".to_string() + }; + + let mut template_function_arity_0 = |name: &str| { + if generate_sources { + builtin_misc_module.push_str(&format!( + r#" +(provide {}) +(define ({}) + (helix.{} *helix.cx*)) +"#, + name, name, name + )); + } + }; + + // Arity 0 + module.register_fn("hx.cx->pos", cx_pos_within_text); + module.register_fn("mode-switch-old", OnModeSwitchEvent::get_old_mode); + module.register_fn("mode-switch-new", OnModeSwitchEvent::get_new_mode); + + template_function_arity_0("hx.cx->pos"); + + let mut template_function_arity_1 = |name: &str| { + if generate_sources { + builtin_misc_module.push_str(&format!( + r#" +(provide {}) +(define ({} arg) + (helix.{} *helix.cx* arg)) +"#, + name, name, name + )); + } + }; + + // Arity 1 + module.register_fn("hx.custom-insert-newline", custom_insert_newline); + module.register_fn("push-component!", push_component); + module.register_fn("pop-last-component!", pop_last_component_by_name); + module.register_fn("enqueue-thread-local-callback", enqueue_command); + module.register_fn("set-status!", set_status); + + template_function_arity_1("pop-last-component!"); + template_function_arity_1("hx.custom-insert-newline"); + template_function_arity_1("push-component!"); + template_function_arity_1("enqueue-thread-local-callback"); + template_function_arity_1("set-status!"); + + module.register_fn("send-lsp-command", send_arbitrary_lsp_command); + if generate_sources { + builtin_misc_module.push_str( + r#" + (provide send-lsp-command) + ;;@doc + ;; Send an lsp command. The `lsp-name` must correspond to an active lsp. + ;; The method name corresponds to the method name that you'd expect to see + ;; with the lsp, and the params can be passed as a hash table. The callback + ;; provided will be called with whatever result is returned from the LSP, + ;; deserialized from json to a steel value. + ;; + ;; # Example + ;; ```scheme + ;; (define (view-crate-graph) + ;; (send-lsp-command "rust-analyzer" + ;; "rust-analyzer/viewCrateGraph" + ;; (hash "full" #f) + ;; ;; Callback to run with the result + ;; (lambda (result) (displayln result)))) + ;; ``` + (define (send-lsp-command lsp-name method-name params callback) + (helix.send-lsp-command *helix.cx* lsp-name method-name params callback)) + "#, + ); + } + + let mut template_function_arity_2 = |name: &str| { + if generate_sources { + builtin_misc_module.push_str(&format!( + r#" +(provide {}) +(define ({} arg1 arg2) + (helix.{} *helix.cx* arg1 arg2)) +"#, + name, name, name + )); + } + }; + + // Arity 2 + module.register_fn( + "enqueue-thread-local-callback-with-delay", + enqueue_command_with_delay, + ); + + // Arity 2 + module.register_fn("helix-await-callback", await_value); + + template_function_arity_2("enqueue-thread-local-callback-with-delay"); + template_function_arity_2("helix-await-callback"); + + if generate_sources { + let mut target_directory = helix_runtime_search_path(); + + if !target_directory.exists() { + std::fs::create_dir(&target_directory).unwrap(); + } + + target_directory.push("misc.scm"); + + std::fs::write(target_directory, builtin_misc_module).unwrap(); + } + + if generate_sources { + configure_lsp_builtins("misc", &module); + } + + engine.register_module(module); +} + +pub fn helix_runtime_search_path() -> PathBuf { + helix_loader::config_dir().join("helix") +} + +pub fn configure_builtin_sources(engine: &mut Engine, generate_sources: bool) { + load_editor_api(engine, generate_sources); + load_theme_api(engine, generate_sources); + load_configuration_api(engine, generate_sources); + load_typed_commands(engine, generate_sources); + load_static_commands(engine, generate_sources); + // Note: This is going to be completely revamped soon. + load_keymap_api(engine, KeyMapApi::new(), generate_sources); + load_rope_api(engine, generate_sources); + load_misc_api(engine, generate_sources); + load_component_api(engine, generate_sources); + + // TODO: Remove this once all of the globals have been moved into their own modules + if generate_sources { + if std::env::var("STEEL_LSP_HOME").is_err() { + eprintln!("Warning: STEEL_LSP_HOME is not set, so the steel lsp will not be configured with helix primitives"); + } + configure_lsp_globals() + } +} + +fn configure_engine_impl(mut engine: Engine) -> Engine { + log::info!("Loading engine!"); + + engine.add_search_directory(helix_loader::config_dir()); + + engine.register_value("*helix.cx*", SteelVal::Void); + engine.register_value("*helix.config*", SteelVal::Void); + engine.register_value( + "*helix.id*", + SteelVal::IntV(engine.engine_id().as_usize() as _), + ); + + // Don't generate source directories here + configure_builtin_sources(&mut engine, false); + + // Hooks + engine.register_fn("register-hook!", register_hook); + engine.register_fn("log::info!", |message: String| log::info!("{}", message)); + + engine.register_fn("fuzzy-match", |pattern: SteelString, items: SteelVal| { + // Match against how they would be rendered? + + if let SteelVal::ListV(l) = items { + let res = helix_core::fuzzy::fuzzy_match( + pattern.as_str(), + l.iter().filter_map(|x| x.as_string().map(|x| x.as_str())), + false, + ); + + return res + .into_iter() + .map(|x| x.0.to_string().into()) + .collect::>(); + } + + return Vec::new(); + }); + + // Find the workspace + engine.register_fn("helix-find-workspace", || { + helix_core::find_workspace().0.to_str().unwrap().to_string() + }); + + engine.register_fn("doc-id->usize", document_id_to_usize); + + engine.register_fn("new-component!", SteelDynamicComponent::new_dyn); + + engine.register_fn( + "acquire-context-lock", + |callback_fn: SteelVal, place: Option| { + match (&callback_fn, &place) { + (SteelVal::Closure(_), Some(SteelVal::CustomStruct(_))) => {} + _ => { + steel::stop!(TypeMismatch => "acquire-context-lock expected a + callback function and a task object") + } + } + + let rooted = callback_fn.as_rooted(); + let rooted_place = place.map(|x| x.as_rooted()); + + let callback = + move |editor: &mut Editor, _compositor: &mut Compositor, jobs: &mut job::Jobs| { + let mut ctx = Context { + register: None, + count: None, + editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs, + }; + + let cloned_func = rooted.value(); + let cloned_place = rooted_place.as_ref().map(|x| x.value()); + + enter_engine(|guard| { + if let Err(e) = guard + .with_mut_reference::(&mut ctx) + // Block until the other thread is finished in its critical + // section... + .consume(move |engine, args| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + + let mut lock = None; + + if let Some(SteelVal::CustomStruct(s)) = cloned_place { + let mutex = s.get_mut_index(0).unwrap(); + lock = Some(mutex_lock(&mutex).unwrap()); + } + + // Acquire lock, wait until its done + let result = + engine.call_function_with_args(cloned_func.clone(), Vec::new()); + + if let Some(SteelVal::CustomStruct(s)) = cloned_place { + match result { + Ok(result) => { + // Store the result of the callback so that the + // next downstream user can handle it. + s.set_index(2, result); + s.set_index(1, SteelVal::BoolV(true)); + mutex_unlock(&lock.unwrap()).unwrap(); + } + + Err(e) => { + return Err(e); + } + } + } + + Ok(()) + }) + { + present_error_inside_engine_context(&mut ctx, guard, e); + } + }) + }; + job::dispatch_blocking_jobs(callback); + + Ok(()) + }, + ); + + engine.register_fn("SteelDynamicComponent?", |object: SteelVal| { + if let SteelVal::Custom(v) = object { + if let Some(wrapped) = v.read().as_any_ref().downcast_ref::() { + return wrapped.inner.as_any().is::(); + } else { + false + } + } else { + false + } + }); + + engine.register_fn( + "prompt", + |prompt: String, callback_fn: SteelVal| -> WrappedDynComponent { + let callback_fn_guard = callback_fn.as_rooted(); + + let prompt = Prompt::new( + prompt.into(), + None, + |_, _| Vec::new(), + move |cx, input, prompt_event| { + log::info!("Calling dynamic prompt callback"); + + if prompt_event != PromptEvent::Validate { + return; + } + + let mut ctx = Context { + register: None, + count: None, + editor: cx.editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs: cx.jobs, + }; + + let cloned_func = callback_fn_guard.value(); + + with_interrupt_handler(|| { + enter_engine(|guard| { + if let Err(e) = guard + .with_mut_reference::(&mut ctx) + .consume(move |engine, args| { + let context = args[0].clone(); + + engine.update_value("*helix.cx*", context); + + engine.call_function_with_args( + cloned_func.clone(), + vec![input.into_steelval().unwrap()], + ) + }) + { + present_error_inside_engine_context(&mut ctx, guard, e); + } + }) + }) + }, + ); + + WrappedDynComponent { + inner: Some(Box::new(prompt)), + } + }, + ); + + engine.register_fn("picker", |values: Vec| -> WrappedDynComponent { + let columns = [PickerColumn::new( + "path", + |item: &PathBuf, root: &PathBuf| { + item.strip_prefix(root) + .unwrap_or(item) + .to_string_lossy() + .into() + }, + )]; + let cwd = helix_stdx::env::current_working_dir(); + + let picker = ui::Picker::new(columns, 0, [], cwd, move |cx, path: &PathBuf, action| { + if let Err(e) = cx.editor.open(path, action) { + let err = if let Some(err) = e.source() { + format!("{}", err) + } else { + format!("unable to open \"{}\"", path.display()) + }; + cx.editor.set_error(err); + } + }) + .with_preview(|_editor, path| Some((PathOrId::Path(path), None))); + + let injector = picker.injector(); + + for file in values { + if injector.push(PathBuf::from(file)).is_err() { + break; + } + } + + WrappedDynComponent { + inner: Some(Box::new(ui::overlay::overlaid(picker))), + } + }); + + engine.register_fn("Component::Text", |contents: String| WrappedDynComponent { + inner: Some(Box::new(crate::ui::Text::new(contents))), + }); + + // Create directory since we can't do that in the current state + engine.register_fn("hx.create-directory", create_directory); + + engine +} + +fn get_highlighted_text(cx: &mut Context) -> String { + let (view, doc) = current_ref!(cx.editor); + let text = doc.text().slice(..); + doc.selection(view.id).primary().slice(text).to_string() +} + +fn current_selection(cx: &mut Context) -> Selection { + let (view, doc) = current_ref!(cx.editor); + doc.selection(view.id).clone() +} + +fn set_selection(cx: &mut Context, selection: Selection) { + let (view, doc) = current!(cx.editor); + doc.set_selection(view.id, selection) +} + +fn current_line_number(cx: &mut Context) -> usize { + let (view, doc) = current_ref!(cx.editor); + helix_core::coords_at_pos( + doc.text().slice(..), + doc.selection(view.id) + .primary() + .cursor(doc.text().slice(..)), + ) + .row +} + +fn get_selection(cx: &mut Context) -> String { + let (view, doc) = current_ref!(cx.editor); + let text = doc.text().slice(..); + + let grapheme_start = doc.selection(view.id).primary().cursor(text); + let grapheme_end = graphemes::next_grapheme_boundary(text, grapheme_start); + + if grapheme_start == grapheme_end { + return "".into(); + } + + let grapheme = text.slice(grapheme_start..grapheme_end).to_string(); + + let printable = grapheme.chars().fold(String::new(), |mut s, c| { + match c { + '\0' => s.push_str("\\0"), + '\t' => s.push_str("\\t"), + '\n' => s.push_str("\\n"), + '\r' => s.push_str("\\r"), + _ => s.push(c), + } + + s + }); + + printable +} + +// TODO: Replace with eval-string +pub fn run_expression_in_engine(cx: &mut Context, text: String) -> anyhow::Result<()> { + let callback = async move { + let call: Box = Box::new( + move |editor: &mut Editor, compositor: &mut Compositor, jobs: &mut job::Jobs| { + let mut ctx = Context { + register: None, + count: None, + editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs, + }; + + let output = enter_engine(|guard| { + guard + .with_mut_reference::(&mut ctx) + .consume(move |engine, args| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + + engine.compile_and_run_raw_program(text.clone()) + }) + }); + + match output { + Ok(output) => { + let (output, _success) = (Tendril::from(format!("{:?}", output)), true); + + let contents = ui::Markdown::new( + format!("```\n{}\n```", output), + editor.syn_loader.clone(), + ); + let popup = Popup::new("engine", contents).position(Some( + helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2), + )); + compositor.replace_or_push("engine", popup); + } + Err(e) => enter_engine(|x| present_error_inside_engine_context(&mut ctx, x, e)), + } + }, + ); + Ok(call) + }; + cx.jobs.local_callback(callback); + + Ok(()) +} + +pub fn load_buffer(cx: &mut Context) -> anyhow::Result<()> { + let (text, path) = { + let (_, doc) = current!(cx.editor); + + let text = doc.text().to_string(); + let path = current_path(cx); + + (text, path) + }; + + let callback = async move { + let call: Box = Box::new( + move |editor: &mut Editor, compositor: &mut Compositor, jobs: &mut job::Jobs| { + let mut ctx = Context { + register: None, + count: None, + editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs, + }; + + let output = enter_engine(|guard| { + guard + .with_mut_reference::(&mut ctx) + .consume(move |engine, args| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + + match path.clone() { + Some(path) => engine.compile_and_run_raw_program_with_path( + // TODO: Figure out why I have to clone this text here. + text.clone(), + PathBuf::from(path), + ), + None => engine.compile_and_run_raw_program(text.clone()), + } + }) + }); + + match output { + Ok(output) => { + let (output, _success) = (Tendril::from(format!("{:?}", output)), true); + + let contents = ui::Markdown::new( + format!("```\n{}\n```", output), + editor.syn_loader.clone(), + ); + let popup = Popup::new("engine", contents).position(Some( + helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2), + )); + compositor.replace_or_push("engine", popup); + } + Err(e) => enter_engine(|x| present_error_inside_engine_context(&mut ctx, x, e)), + } + }, + ); + Ok(call) + }; + cx.jobs.local_callback(callback); + + Ok(()) +} + +fn get_helix_scm_path() -> String { + helix_module_file().to_str().unwrap().to_string() +} + +fn get_init_scm_path() -> String { + steel_init_file().to_str().unwrap().to_string() +} + +/// Get the current path! See if this can be done _without_ this function? +// TODO: +fn current_path(cx: &mut Context) -> Option { + let current_focus = cx.editor.tree.focus; + let view = cx.editor.tree.get(current_focus); + let doc = &view.doc; + // Lifetime of this needs to be tied to the existing document + let current_doc = cx.editor.documents.get(doc); + current_doc.and_then(|x| x.path().and_then(|x| x.to_str().map(|x| x.to_string()))) +} + +fn set_scratch_buffer_name(cx: &mut Context, name: String) { + let current_focus = cx.editor.tree.focus; + let view = cx.editor.tree.get(current_focus); + let doc = &view.doc; + // Lifetime of this needs to be tied to the existing document + let current_doc = cx.editor.documents.get_mut(doc); + + if let Some(current_doc) = current_doc { + current_doc.name = Some(name); + } +} + +fn cx_current_focus(cx: &mut Context) -> helix_view::ViewId { + cx.editor.tree.focus +} + +fn cx_get_document_id(cx: &mut Context, view_id: helix_view::ViewId) -> DocumentId { + cx.editor.tree.get(view_id).doc +} + +fn document_id_to_text(cx: &mut Context, doc_id: DocumentId) -> Option { + cx.editor + .documents + .get(&doc_id) + .map(|x| SteelRopeSlice::new(x.text().clone())) +} + +fn cx_is_document_in_view(cx: &mut Context, doc_id: DocumentId) -> Option { + cx.editor + .tree + .traverse() + .find(|(_, v)| v.doc == doc_id) + .map(|(id, _)| id) +} + +fn cx_document_exists(cx: &mut Context, doc_id: DocumentId) -> bool { + cx.editor.documents.get(&doc_id).is_some() +} + +fn document_path(cx: &mut Context, doc_id: DocumentId) -> Option { + cx.editor + .documents + .get(&doc_id) + .and_then(|doc| doc.path().and_then(|x| x.to_str()).map(|x| x.to_string())) +} + +fn cx_editor_all_documents(cx: &mut Context) -> Vec { + cx.editor.documents.keys().copied().collect() +} + +fn cx_switch(cx: &mut Context, doc_id: DocumentId) { + cx.editor.switch(doc_id, Action::VerticalSplit) +} + +fn cx_switch_action(cx: &mut Context, doc_id: DocumentId, action: Action) { + cx.editor.switch(doc_id, action) +} + +fn cx_get_mode(cx: &mut Context) -> Mode { + cx.editor.mode +} + +fn cx_set_mode(cx: &mut Context, mode: Mode) { + cx.editor.mode = mode +} + +// Overlay the dynamic component, see what happens? +// Probably need to pin the values to this thread - wrap it in a shim which pins the value +// to this thread? - call methods on the thread local value? +fn push_component(cx: &mut Context, component: &mut WrappedDynComponent) { + log::info!("Pushing dynamic component!"); + + let inner = component.inner.take().unwrap(); + + let callback = async move { + let call: Box = Box::new( + move |_editor: &mut Editor, compositor: &mut Compositor, _| compositor.push(inner), + ); + Ok(call) + }; + cx.jobs.local_callback(callback); +} + +fn pop_last_component_by_name(cx: &mut Context, name: SteelString) { + let callback = async move { + let call: Box = Box::new( + move |_editor: &mut Editor, compositor: &mut Compositor, _jobs: &mut job::Jobs| { + compositor.remove_by_dynamic_name(&name); + }, + ); + Ok(call) + }; + cx.jobs.local_callback(callback); +} + +fn set_status(cx: &mut Context, value: SteelVal) { + cx.editor.set_status(value.to_string()) +} + +fn enqueue_command(cx: &mut Context, callback_fn: SteelVal) { + let rooted = callback_fn.as_rooted(); + + let callback = async move { + let call: Box = Box::new( + move |editor: &mut Editor, _compositor: &mut Compositor, jobs: &mut job::Jobs| { + let mut ctx = Context { + register: None, + count: None, + editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs, + }; + + let cloned_func = rooted.value(); + + enter_engine(|guard| { + if let Err(e) = guard + .with_mut_reference::(&mut ctx) + .consume(move |engine, args| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + + engine.call_function_with_args(cloned_func.clone(), Vec::new()) + }) + { + present_error_inside_engine_context(&mut ctx, guard, e); + } + }) + }, + ); + Ok(call) + }; + cx.jobs.local_callback(callback); +} + +// Apply arbitrary delay for update rate... +fn enqueue_command_with_delay(cx: &mut Context, delay: SteelVal, callback_fn: SteelVal) { + let rooted = callback_fn.as_rooted(); + + let callback = async move { + let delay = delay.int_or_else(|| panic!("FIX ME")).unwrap(); + + tokio::time::sleep(tokio::time::Duration::from_millis(delay as u64)).await; + + let call: Box = Box::new( + move |editor: &mut Editor, _compositor: &mut Compositor, jobs: &mut job::Jobs| { + let mut ctx = Context { + register: None, + count: None, + editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs, + }; + + let cloned_func = rooted.value(); + + enter_engine(|guard| { + if let Err(e) = guard + .with_mut_reference::(&mut ctx) + .consume(move |engine, args| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + + engine.call_function_with_args(cloned_func.clone(), Vec::new()) + }) + { + present_error_inside_engine_context(&mut ctx, guard, e); + } + }) + }, + ); + Ok(call) + }; + cx.jobs.local_callback(callback); +} + +// value _must_ be a future here. Otherwise awaiting will cause problems! +fn await_value(cx: &mut Context, value: SteelVal, callback_fn: SteelVal) { + if !value.is_future() { + return; + } + + let rooted = callback_fn.as_rooted(); + + let callback = async move { + let future_value = value.as_future().unwrap().await; + + let call: Box = Box::new( + move |editor: &mut Editor, _compositor: &mut Compositor, jobs: &mut job::Jobs| { + let mut ctx = Context { + register: None, + count: None, + editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs, + }; + + let cloned_func = rooted.value(); + + match future_value { + Ok(inner) => { + let callback = move |engine: &mut Engine, args: Vec| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + + // args.push(inner); + engine.call_function_with_args(cloned_func.clone(), vec![inner]) + }; + + enter_engine(|guard| { + if let Err(e) = guard + .with_mut_reference::(&mut ctx) + .consume_once(callback) + { + present_error_inside_engine_context(&mut ctx, guard, e); + } + }) + } + Err(e) => enter_engine(|x| present_error_inside_engine_context(&mut ctx, x, e)), + } + }, + ); + Ok(call) + }; + cx.jobs.local_callback(callback); +} +// Check that we successfully created a directory? +fn create_directory(path: String) { + let path = helix_stdx::path::canonicalize(&PathBuf::from(path)); + + if path.exists() { + return; + } else { + std::fs::create_dir(path).unwrap(); + } +} + +pub fn cx_pos_within_text(cx: &mut Context) -> usize { + let (view, doc) = current_ref!(cx.editor); + + let text = doc.text().slice(..); + + let selection = doc.selection(view.id).clone(); + + let pos = selection.primary().cursor(text); + + pos +} + +pub fn get_helix_cwd(_cx: &mut Context) -> Option { + helix_stdx::env::current_working_dir() + .as_os_str() + .to_str() + .map(|x| x.into()) +} + +// Special newline... +pub fn custom_insert_newline(cx: &mut Context, indent: String) { + let (view, doc) = current_ref!(cx.editor); + + // let rope = doc.text().clone(); + + let text = doc.text().slice(..); + + let contents = doc.text(); + let selection = doc.selection(view.id).clone(); + let mut ranges = helix_core::SmallVec::with_capacity(selection.len()); + + // TODO: this is annoying, but we need to do it to properly calculate pos after edits + let mut global_offs = 0; + + let mut transaction = + helix_core::Transaction::change_by_selection(contents, &selection, |range| { + let pos = range.cursor(text); + + let prev = if pos == 0 { + ' ' + } else { + contents.char(pos - 1) + }; + let curr = contents.get_char(pos).unwrap_or(' '); + + let current_line = text.char_to_line(pos); + let line_is_only_whitespace = text + .line(current_line) + .chars() + .all(|char| char.is_ascii_whitespace()); + + let mut new_text = String::new(); + + // If the current line is all whitespace, insert a line ending at the beginning of + // the current line. This makes the current line empty and the new line contain the + // indentation of the old line. + let (from, to, local_offs) = if line_is_only_whitespace { + let line_start = text.line_to_char(current_line); + new_text.push_str(doc.line_ending.as_str()); + + (line_start, line_start, new_text.chars().count()) + } else { + // If we are between pairs (such as brackets), we want to + // insert an additional line which is indented one level + // more and place the cursor there + let on_auto_pair = doc + .auto_pairs(cx.editor) + .and_then(|pairs| pairs.get(prev)) + .map_or(false, |pair| pair.open == prev && pair.close == curr); + + let local_offs = if on_auto_pair { + let inner_indent = indent.clone() + doc.indent_style.as_str(); + new_text.reserve_exact(2 + indent.len() + inner_indent.len()); + new_text.push_str(doc.line_ending.as_str()); + new_text.push_str(&inner_indent); + let local_offs = new_text.chars().count(); + new_text.push_str(doc.line_ending.as_str()); + new_text.push_str(&indent); + local_offs + } else { + new_text.reserve_exact(1 + indent.len()); + new_text.push_str(doc.line_ending.as_str()); + new_text.push_str(&indent); + new_text.chars().count() + }; + + (pos, pos, local_offs) + }; + + let new_range = if doc.restore_cursor { + // when appending, extend the range by local_offs + Range::new( + range.anchor + global_offs, + range.head + local_offs + global_offs, + ) + } else { + // when inserting, slide the range by local_offs + Range::new( + range.anchor + local_offs + global_offs, + range.head + local_offs + global_offs, + ) + }; + + // TODO: range replace or extend + // range.replace(|range| range.is_empty(), head); -> fn extend if cond true, new head pos + // can be used with cx.mode to do replace or extend on most changes + ranges.push(new_range); + global_offs += new_text.chars().count(); + + (from, to, Some(new_text.into())) + }); + + transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index())); + + let (view, doc) = current!(cx.editor); + doc.apply(&transaction, view.id); +} + +// fn search_in_directory(cx: &mut Context, directory: String) { +// let buf = PathBuf::from(directory); +// let search_path = expand_tilde(&buf); +// let path = search_path.to_path_buf(); +// crate::commands::search_in_directory(cx, path); +// } + +// TODO: Result should create unrecoverable result, and should have a special +// recoverable result - that way we can handle both, not one in particular +fn regex_selection(cx: &mut Context, regex: String) { + if let Ok(regex) = helix_stdx::rope::Regex::new(®ex) { + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + if let Some(selection) = + helix_core::selection::select_on_matches(text, doc.selection(view.id), ®ex) + { + doc.set_selection(view.id, selection); + } + } +} + +fn replace_selection(cx: &mut Context, value: String) { + let (view, doc) = current!(cx.editor); + + let selection = doc.selection(view.id); + let transaction = + helix_core::Transaction::change_by_selection(doc.text(), selection, |range| { + if !range.is_empty() { + (range.from(), range.to(), Some(value.to_owned().into())) + } else { + (range.from(), range.to(), None) + } + }); + + doc.apply(&transaction, view.id); +} + +// TODO: Remove this! +fn move_window_to_the_left(cx: &mut Context) { + while cx + .editor + .tree + .swap_split_in_direction(helix_view::tree::Direction::Left) + .is_some() + {} +} + +// TODO: Remove this! +fn move_window_to_the_right(cx: &mut Context) { + while cx + .editor + .tree + .swap_split_in_direction(helix_view::tree::Direction::Right) + .is_some() + {} +} + +fn send_arbitrary_lsp_command( + cx: &mut Context, + name: SteelString, + command: SteelString, + // Arguments - these will be converted to some json stuff + json_argument: Option, + callback_fn: SteelVal, +) -> anyhow::Result<()> { + let argument = json_argument.map(|x| serde_json::Value::try_from(x).unwrap()); + + let (_view, doc) = current!(cx.editor); + + let language_server_id = anyhow::Context::context( + doc.language_servers().find(|x| x.name() == name.as_str()), + "Unable to find the language server specified!", + )? + .id(); + + let future = match cx + .editor + .language_server_by_id(language_server_id) + .and_then(|language_server| { + language_server.non_standard_extension(command.to_string(), argument) + }) { + Some(future) => future, + None => { + // TODO: Come up with a better message once we check the capabilities for + // the arbitrary thing you're trying to do, since for now the above actually + // always returns a `Some` + cx.editor.set_error( + "Language server does not support whatever command you just tried to do", + ); + return Ok(()); + } + }; + + let rooted = callback_fn.as_rooted(); + + create_callback(cx, future, rooted)?; + + Ok(()) +} + +fn create_callback + 'static>( + cx: &mut Context, + future: impl std::future::Future> + 'static, + rooted: steel::RootedSteelVal, +) -> Result<(), anyhow::Error> { + let callback = async move { + // Result of the future - this will be whatever we get back + // from the lsp call + let res = future.await?; + + let call: Box = Box::new( + move |editor: &mut Editor, _compositor: &mut Compositor, jobs: &mut job::Jobs| { + let mut ctx = Context { + register: None, + count: None, + editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs, + }; + + let cloned_func = rooted.value(); + + enter_engine(move |guard| match TryInto::::try_into(res) { + Ok(result) => { + if let Err(e) = guard + .with_mut_reference::(&mut ctx) + .consume(move |engine, args| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + + engine.call_function_with_args( + cloned_func.clone(), + vec![result.clone()], + ) + }) + { + present_error_inside_engine_context(&mut ctx, guard, e); + } + } + Err(e) => { + present_error_inside_engine_context(&mut ctx, guard, e); + } + }) + }, + ); + Ok(call) + }; + cx.jobs.local_callback(callback); + Ok(()) +} diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 4e912127c3d6..fe897e354ed4 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1,4 +1,3 @@ -use std::fmt::Write; use std::io::BufReader; use std::ops::{self, Deref}; @@ -17,6 +16,8 @@ use helix_view::expansion; use serde_json::Value; use ui::completers::{self, Completer}; +use std::fmt::Write; + #[derive(Clone)] pub struct TypableCommand { pub name: &'static str, @@ -48,21 +49,21 @@ pub struct CommandCompleter { } impl CommandCompleter { - const fn none() -> Self { + pub const fn none() -> Self { Self { positional_args: &[], var_args: completers::none, } } - const fn positional(completers: &'static [Completer]) -> Self { + pub const fn positional(completers: &'static [Completer]) -> Self { Self { positional_args: completers, var_args: completers::none, } } - const fn all(completer: Completer) -> Self { + pub const fn all(completer: Completer) -> Self { Self { positional_args: &[], var_args: completer, @@ -669,6 +670,8 @@ pub(super) fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> let modified_ids: Vec<_> = editor .documents() .filter(|doc| doc.is_modified()) + // Named scratch documents should not be included here + .filter(|doc| doc.name.is_none()) .map(|doc| doc.id()) .collect(); @@ -721,7 +724,13 @@ pub fn write_all_impl( if !doc.is_modified() { return None; } - if doc.path().is_none() { + + // This is a named buffer. We'll skip it in the saves for now + if doc.name.is_some() { + return None; + } + + if doc.path().is_none() && doc.name.is_none() { if options.write_scratch { errors.push("cannot write a buffer without a filename"); } @@ -924,21 +933,42 @@ fn theme(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow // Ensures that a preview theme gets cleaned up if the user backspaces until the prompt is empty. cx.editor.unset_theme_preview(); } else if let Some(theme_name) = args.first() { - if let Ok(theme) = cx.editor.theme_loader.load(theme_name) { + // if let Ok(theme) = cx.editor.theme_loader.load(theme_name) { + // if !(true_color || theme.is_16_color()) { + // bail!("Unsupported theme: theme requires true color support"); + // } + // cx.editor.set_theme_preview(theme); + // }; + + if let Ok(theme) = cx.editor.theme_loader.load(theme_name).or_else(|_| { + cx.editor + .user_defined_themes + .get(theme_name) + .ok_or_else(|| anyhow::anyhow!("Could not load theme")) + .cloned() + }) { if !(true_color || theme.is_16_color()) { bail!("Unsupported theme: theme requires true color support"); } cx.editor.set_theme_preview(theme); - }; + } }; } PromptEvent::Validate => { if let Some(theme_name) = args.first() { - let theme = cx - .editor - .theme_loader - .load(theme_name) - .map_err(|err| anyhow::anyhow!("Could not load theme: {}", err))?; + let theme = cx.editor.theme_loader.load(theme_name).or_else(|_| { + cx.editor + .user_defined_themes + .get(theme_name) + .ok_or_else(|| anyhow::anyhow!("Could not load theme")) + .cloned() + })?; + + // let theme = cx + // .editor + // .theme_loader + // .load(theme_name) + // .map_err(|err| anyhow::anyhow!("Could not load theme: {}", err))?; if !(true_color || theme.is_16_color()) { bail!("Unsupported theme: theme requires true color support"); } @@ -2312,6 +2342,39 @@ fn pipe_impl( Ok(()) } +fn run_shell_command_text( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + let shell = cx.editor.config().shell.clone(); + let args = args.join(" "); + + let callback = async move { + let output = shell_impl_async(&shell, &args, None).await?; + let call: job::Callback = Callback::EditorCompositor(Box::new( + move |editor: &mut Editor, compositor: &mut Compositor| { + if !output.is_empty() { + let contents = ui::Text::new(format!("{}", output)); + let popup = Popup::new("shell", contents).position(Some( + helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2), + )); + compositor.replace_or_push("shell", popup); + } + editor.set_status("Command succeeded"); + }, + )); + Ok(call) + }; + cx.jobs.callback(callback); + + Ok(()) +} + fn run_shell_command( cx: &mut compositor::Context, args: Args, @@ -3464,6 +3527,14 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ completer: SHELL_COMPLETER, signature: SHELL_SIGNATURE, }, + // TypableCommand { + // name: "run-shell-command-text", + // aliases: &["sh"], + // doc: "Run a shell command", + // fun: run_shell_command_text, + // completer: SHELL_COMPLETER, + // signature: CommandSignature::all(completers::filename) + // }, TypableCommand { name: "reset-diff-change", aliases: &["diffget", "diffg"], @@ -3583,7 +3654,41 @@ fn execute_command_line( match typed::TYPABLE_COMMAND_MAP.get(command) { Some(cmd) => execute_command(cx, cmd, rest, event), - None if event == PromptEvent::Validate => Err(anyhow!("no such command: '{command}'")), + None if event == PromptEvent::Validate => { + let parts = rest.split_whitespace().collect::>(); + + if ScriptingEngine::call_typed_command(cx, input, &parts, event) { + // Engine handles the other cases + if event == PromptEvent::Validate { + let mappable_command = MappableCommand::Typable { + name: input.to_string(), + args: String::default(), + doc: "".to_string(), + }; + + let mut ctx = Context { + register: None, + count: None, + editor: cx.editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs: cx.jobs, + }; + + // // TODO: Figure this out? + helix_event::dispatch(crate::events::PostCommand { + command: &mappable_command, + cx: &mut ctx, + }); + + Ok(()) + } else { + Ok(()) + } + } else { + Err(anyhow!("no such command: '{command}'")) + } + } None => Ok(()), } } @@ -3604,7 +3709,30 @@ pub(super) fn execute_command( .expect("arg parsing cannot fail when validation is turned off") }; - (cmd.fun)(cx, args, event).map_err(|err| anyhow!("'{}': {err}", cmd.name)) + let res = (cmd.fun)(cx, args, event).map_err(|err| anyhow!("'{}': {err}", cmd.name)); + + let mappable_command = MappableCommand::Typable { + name: cmd.name.to_string(), + args: String::new(), + doc: "".to_string(), + }; + + let mut ctx = Context { + register: None, + count: None, + editor: cx.editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs: cx.jobs, + }; + + // // TODO: Figure this out? + helix_event::dispatch(crate::events::PostCommand { + command: &mappable_command, + cx: &mut ctx, + }); + + res } #[allow(clippy::unnecessary_unwrap)] @@ -3627,8 +3755,14 @@ pub(super) fn command_mode(cx: &mut Context) { } fn command_line_doc(input: &str) -> Option> { - let (command, _, _) = command_line::split(input); - let command = TYPABLE_COMMAND_MAP.get(command)?; + let (command_name, _, _) = command_line::split(input); + let command = TYPABLE_COMMAND_MAP.get(command_name); + + if command.is_none() { + return ScriptingEngine::get_doc_for_identifier(command_name).map(|x| x.into()); + } + + let command = command?; if command.aliases.is_empty() && command.signature.flags.is_empty() { return Some(Cow::Borrowed(command.doc)); @@ -3704,7 +3838,10 @@ fn complete_command_line(editor: &Editor, input: &str) -> Vec; pub enum EventResult { Ignored(Option), Consumed(Option), + ConsumedWithoutRerender, } use crate::job::Jobs; @@ -73,6 +74,10 @@ pub trait Component: Any + AnyComponent { fn id(&self) -> Option<&'static str> { None } + + fn name(&self) -> Option<&str> { + None + } } pub struct Compositor { @@ -136,6 +141,14 @@ impl Compositor { Some(self.layers.remove(idx)) } + pub fn remove_by_dynamic_name(&mut self, id: &str) -> Option> { + let idx = self + .layers + .iter() + .position(|layer| layer.name() == Some(id))?; + Some(self.layers.remove(idx)) + } + pub fn handle_event(&mut self, event: &Event, cx: &mut Context) -> bool { // If it is a key event, a macro is being recorded, and a macro isn't being replayed, // push the key event to the recording. @@ -162,6 +175,10 @@ impl Compositor { consumed = true; break; } + // Swallow the event, but don't trigger a re-render + EventResult::ConsumedWithoutRerender => { + break; + } EventResult::Ignored(Some(callback)) => { callbacks.push(callback); } @@ -178,7 +195,9 @@ impl Compositor { pub fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { for layer in &mut self.layers { - layer.render(area, surface, cx); + if layer.should_update() { + layer.render(area, surface, cx) + }; } } diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index bcba8d8e1d45..a0873c16cd0f 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -64,6 +64,7 @@ impl Config { global.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig)); let local_config: Result = local.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig)); + let res = match (global_config, local_config) { (Ok(global), Ok(local)) => { let mut keys = keymap::default(); @@ -100,6 +101,7 @@ impl Config { if let Some(keymap) = config.keys { merge_keys(&mut keys, keymap); } + Config { theme: config.theme, keys, @@ -122,6 +124,7 @@ impl Config { fs::read_to_string(helix_loader::config_file()).map_err(ConfigLoadError::Error); let local_config = fs::read_to_string(helix_loader::workspace_config_file()) .map_err(ConfigLoadError::Error); + Config::load(global_config, local_config) } } diff --git a/helix-term/src/job.rs b/helix-term/src/job.rs index 72ed892ddf9a..2b9da4c29127 100644 --- a/helix-term/src/job.rs +++ b/helix-term/src/job.rs @@ -5,13 +5,19 @@ use once_cell::sync::OnceCell; use crate::compositor::Compositor; +use futures_util::future::LocalBoxFuture; use futures_util::future::{BoxFuture, Future, FutureExt}; use futures_util::stream::{FuturesUnordered, StreamExt}; use tokio::sync::mpsc::{channel, Receiver, Sender}; +pub type EditorCompositorJobsCallback = + Box; pub type EditorCompositorCallback = Box; pub type EditorCallback = Box; +pub type ThreadLocalEditorCompositorCallback = + Box; + runtime_local! { static JOB_QUEUE: OnceCell> = OnceCell::new(); } @@ -32,7 +38,15 @@ pub fn dispatch_blocking(job: impl FnOnce(&mut Editor, &mut Compositor) + Send + send_blocking(jobs, Callback::EditorCompositor(Box::new(job))) } +pub fn dispatch_blocking_jobs( + job: impl FnOnce(&mut Editor, &mut Compositor, &mut Jobs) + Send + 'static, +) { + let jobs = JOB_QUEUE.wait(); + send_blocking(jobs, Callback::EditorCompositorJobs(Box::new(job))) +} + pub enum Callback { + EditorCompositorJobs(EditorCompositorJobsCallback), EditorCompositor(EditorCompositorCallback), Editor(EditorCallback), } @@ -45,9 +59,13 @@ pub struct Job { pub wait: bool, } +pub type ThreadLocalJob = + LocalBoxFuture<'static, anyhow::Result>>; + pub struct Jobs { - /// jobs that need to complete before we exit. + /// jobs the ones that need to complete before we exit. pub wait_futures: FuturesUnordered, + pub local_futures: FuturesUnordered, pub callbacks: Receiver, pub status_messages: Receiver, } @@ -83,6 +101,7 @@ impl Jobs { let status_messages = helix_event::status::setup(); Self { wait_futures: FuturesUnordered::new(), + local_futures: FuturesUnordered::new(), callbacks: rx, status_messages, } @@ -99,8 +118,18 @@ impl Jobs { self.add(Job::with_callback(f)); } + pub fn local_callback< + F: Future> + 'static, + >( + &mut self, + f: F, + ) { + self.local_futures + .push(f.map(|r| r.map(Some)).boxed_local()); + } + pub fn handle_callback( - &self, + &mut self, editor: &mut Editor, compositor: &mut Compositor, call: anyhow::Result>, @@ -108,6 +137,7 @@ impl Jobs { match call { Ok(None) => {} Ok(Some(call)) => match call { + Callback::EditorCompositorJobs(call) => call(editor, compositor, self), Callback::EditorCompositor(call) => call(editor, compositor), Callback::Editor(call) => call(editor), }, @@ -117,6 +147,21 @@ impl Jobs { } } + pub fn handle_local_callback( + &mut self, + editor: &mut Editor, + compositor: &mut Compositor, + call: anyhow::Result>, + ) { + match call { + Ok(None) => {} + Ok(Some(call)) => call(editor, compositor, self), + Err(e) => { + editor.set_error(format!("Sync job failed: {}", e)); + } + } + } + pub fn add(&self, j: Job) { if j.wait { self.wait_futures.push(j.future); diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index d8227b500ee7..d881421a7861 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -235,6 +235,23 @@ impl KeyTrie { res } + pub fn apply(&mut self, func: &mut dyn FnMut(&mut MappableCommand)) { + match self { + KeyTrie::MappableCommand(MappableCommand::Macro { .. }) => {} + KeyTrie::MappableCommand(cmd) => (func)(cmd), + KeyTrie::Node(next) => { + for (_, trie) in &mut next.map { + trie.apply(func); + } + } + KeyTrie::Sequence(seq) => { + for s in seq { + (func)(s) + } + } + }; + } + pub fn node(&self) -> Option<&KeyTrieNode> { match *self { KeyTrie::Node(ref node) => Some(node), @@ -326,12 +343,14 @@ impl Keymaps { .is_some_and(|node| node.contains_key(&key)) } - /// Lookup `key` in the keymap to try and find a command to execute. Escape - /// key cancels pending keystrokes. If there are no pending keystrokes but a - /// sticky node is in use, it will be cleared. - pub fn get(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult { + pub(crate) fn get_with_map( + &mut self, + keymaps: &HashMap, + mode: Mode, + key: KeyEvent, + ) -> KeymapResult { // TODO: remove the sticky part and look up manually - let keymaps = &*self.map(); + // let keymaps = &*self.map(); let keymap = &keymaps[&mode]; if key!(Esc) == key { @@ -379,6 +398,13 @@ impl Keymaps { None => KeymapResult::Cancelled(self.state.drain(..).collect()), } } + + /// Lookup `key` in the keymap to try and find a command to execute. Escape + /// key cancels pending keystrokes. If there are no pending keystrokes but a + /// sticky node is in use, it will be cleared. + pub fn get(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult { + self.get_with_map(&*self.map(), mode, key) + } } impl Default for Keymaps { diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 31ab85cff84f..6715f56c294e 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -4,6 +4,7 @@ use helix_loader::VERSION_AND_GIT_HASH; use helix_term::application::Application; use helix_term::args::Args; use helix_term::config::{Config, ConfigLoadError}; +use indexmap::map::MutableKeys; fn setup_logging(verbosity: u64) -> Result<()> { let mut base_config = fern::Dispatch::new(); @@ -40,7 +41,7 @@ fn main() -> Result<()> { #[tokio::main] async fn main_impl() -> Result { - let args = Args::parse_args().context("could not parse arguments")?; + let mut args = Args::parse_args().context("could not parse arguments")?; helix_loader::initialize_config_file(args.config_file.clone()); helix_loader::initialize_log_file(args.log_file.clone()); @@ -114,6 +115,14 @@ FLAGS: setup_logging(args.verbosity).context("failed to initialize logging")?; + // Initialize the engine before we boot up! + helix_term::commands::ScriptingEngine::initialize(); + + // Before setting the working directory, resolve all the paths in args.files + for (path, _) in args.files.iter_mut2() { + *path = helix_stdx::path::canonicalize(&*path); + } + // NOTE: Set the working directory early so the correct configuration is loaded. Be aware that // Application::new() depends on this logic so it must be updated if this changes. if let Some(path) = &args.working_directory { diff --git a/helix-term/src/ui/document.rs b/helix-term/src/ui/document.rs index 8423ae8e437a..4ca2432290ed 100644 --- a/helix-term/src/ui/document.rs +++ b/helix-term/src/ui/document.rs @@ -72,6 +72,7 @@ impl> Iterator for StyleIter<'_, H> { } } + #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub struct LinePos { /// Indicates whether the given visual line @@ -102,6 +103,7 @@ pub fn render_document( Position::new(offset.vertical_offset, offset.horizontal_offset), viewport, ); + render_text( &mut renderer, doc.text().slice(..), diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 6be5657477bc..91ea6ff79cd8 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1,5 +1,5 @@ use crate::{ - commands::{self, OnKeyCallback, OnKeyCallbackKind}, + commands::{self, engine::ScriptingEngine, OnKeyCallback, OnKeyCallbackKind}, compositor::{Component, Context, Event, EventResult}, events::{OnModeSwitch, PostCommand}, handlers::completion::CompletionItem, @@ -887,7 +887,11 @@ impl EditorView { ) -> Option { let mut last_mode = mode; self.pseudo_pending.extend(self.keymaps.pending()); - let key_result = self.keymaps.get(mode, event); + + // Check the engine for any buffer specific keybindings first + let key_result = ScriptingEngine::handle_keymap_event(self, mode, cxt, event) + .unwrap_or_else(|| self.keymaps.get(mode, event)); + cxt.editor.autoinfo = self.keymaps.sticky().map(|node| node.infobox()); let mut execute_command = |command: &commands::MappableCommand| { @@ -1551,6 +1555,22 @@ impl Component for EditorView { _ => false, }; + let mut area = area; + + // TODO: This may need to get looked at! + if let Some(top) = cx.editor.editor_clipping.top { + area = area.clip_top(top); + } + if let Some(bottom) = cx.editor.editor_clipping.bottom { + area = area.clip_bottom(bottom); + } + if let Some(left) = cx.editor.editor_clipping.left { + area = area.clip_left(left); + } + if let Some(right) = cx.editor.editor_clipping.right { + area = area.clip_right(right); + } + // -1 for commandline and -1 for bufferline let mut editor_area = area.clip_bottom(1); if use_bufferline { diff --git a/helix-term/src/ui/extension.rs b/helix-term/src/ui/extension.rs new file mode 100644 index 000000000000..71552c433fd1 --- /dev/null +++ b/helix-term/src/ui/extension.rs @@ -0,0 +1,11 @@ +#[cfg(feature = "steel")] +mod steel_implementations { + + use crate::{ + compositor::Component, + ui::{Popup, Text}, + }; + + impl steel::rvals::Custom for Text {} + impl steel::rvals::Custom for Popup {} +} diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index a76adbe211d8..c243f7542997 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -1,6 +1,7 @@ mod completion; mod document; pub(crate) mod editor; +mod extension; mod info; pub mod lsp; mod markdown; @@ -399,13 +400,18 @@ pub mod completers { .collect() } - pub fn theme(_editor: &Editor, input: &str) -> Vec { + pub fn theme(editor: &Editor, input: &str) -> Vec { let mut names = theme::Loader::read_names(&helix_loader::config_dir().join("themes")); for rt_dir in helix_loader::runtime_dirs() { names.extend(theme::Loader::read_names(&rt_dir.join("themes"))); } + names.push("default".into()); names.push("base16_default".into()); + + // Include any user defined themes as well + names.extend(editor.user_defined_themes.keys().map(|x| x.into())); + names.sort(); names.dedup(); diff --git a/helix-term/src/ui/overlay.rs b/helix-term/src/ui/overlay.rs index ff184d4073fa..2dcbcdc920ed 100644 --- a/helix-term/src/ui/overlay.rs +++ b/helix-term/src/ui/overlay.rs @@ -15,6 +15,12 @@ pub struct Overlay { pub calc_child_size: Box Rect>, } +// TODO: For this to be sound, all of the various functions +// have to now be marked as send + sync + 'static. Annoying, +// and something I'll look into with steel. +unsafe impl Send for Overlay {} +unsafe impl Sync for Overlay {} + /// Surrounds the component with a margin of 5% on each side, and an additional 2 rows at the bottom pub fn overlaid(content: T) -> Overlay { Overlay { diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 03adeb05bbf5..3af9474a487a 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -47,6 +47,12 @@ pub struct Prompt { language: Option<(&'static str, Arc>)>, } +// TODO: For this to be sound, all of the various functions +// have to now be marked as send + sync + 'static. Annoying, +// and something I'll look into with steel. +unsafe impl Send for Prompt {} +unsafe impl Sync for Prompt {} + #[derive(Clone, Copy, PartialEq, Eq)] pub enum PromptEvent { /// The prompt input has been updated. diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 7437cbd074e5..49c818fb742a 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -424,6 +424,7 @@ where let path = rel_path .as_ref() .map(|p| p.to_string_lossy()) + .or_else(|| context.doc.name.as_ref().map(|x| x.into())) .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into()); format!(" {} ", path) }; @@ -483,6 +484,7 @@ where let path = rel_path .as_ref() .and_then(|p| p.file_name().map(|s| s.to_string_lossy())) + .or_else(|| context.doc.name.as_ref().map(|x| x.into())) .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into()); format!(" {} ", path) }; diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml index 2b5767a58b57..d4db46ca5e2a 100644 --- a/helix-tui/Cargo.toml +++ b/helix-tui/Cargo.toml @@ -13,6 +13,7 @@ homepage.workspace = true [features] default = ["crossterm"] +steel = ["dep:steel-core", "helix-view/steel", "helix-core/steel"] [dependencies] helix-view = { path = "../helix-view", features = ["term"] } @@ -25,3 +26,5 @@ crossterm = { version = "0.28", optional = true } termini = "1.0" once_cell = "1.21" log = "~0.4" + +steel-core = { workspace = true, optional = true } diff --git a/helix-tui/src/extension.rs b/helix-tui/src/extension.rs new file mode 100644 index 000000000000..c046137cebcc --- /dev/null +++ b/helix-tui/src/extension.rs @@ -0,0 +1,20 @@ +#[cfg(feature = "steel")] +mod steel_implementations { + + use crate::{ + buffer::Buffer, + text::Text, + widgets::{Block, List, Paragraph, Table}, + }; + + use steel::{gc::unsafe_erased_pointers::CustomReference, rvals::Custom}; + + impl CustomReference for Buffer {} + impl Custom for Block<'static> {} + impl Custom for List<'static> {} + impl Custom for Paragraph<'static> {} + impl Custom for Table<'static> {} + impl Custom for Text<'static> {} + + steel::custom_reference!(Buffer); +} diff --git a/helix-tui/src/lib.rs b/helix-tui/src/lib.rs index 59327d7c348d..a4ffc186c713 100644 --- a/helix-tui/src/lib.rs +++ b/helix-tui/src/lib.rs @@ -130,6 +130,7 @@ pub mod backend; pub mod buffer; +pub mod extension; pub mod layout; pub mod symbols; pub mod terminal; diff --git a/helix-tui/src/widgets/list.rs b/helix-tui/src/widgets/list.rs index 4b0fc02f45bc..5e9add4b42b9 100644 --- a/helix-tui/src/widgets/list.rs +++ b/helix-tui/src/widgets/list.rs @@ -1,12 +1,12 @@ use crate::{ buffer::Buffer, - layout::{Corner, Rect}, - style::Style, + layout::Corner, text::Text, - widgets::{Block, StatefulWidget, Widget}, + widgets::{Block, Widget}, }; +use helix_core::unicode::width::UnicodeWidthStr; +use helix_view::graphics::{Rect, Style}; use std::iter::{self, Iterator}; -use unicode_width::UnicodeWidthStr; #[derive(Debug, Clone)] pub struct ListState { @@ -131,10 +131,8 @@ impl<'a> List<'a> { } } -impl<'a> StatefulWidget for List<'a> { - type State = ListState; - - fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { +impl<'a> List<'a> { + fn render_list(mut self, area: Rect, buf: &mut Buffer, state: &mut ListState) { buf.set_style(area, self.style); let list_area = match self.block.take() { Some(b) => { @@ -244,6 +242,6 @@ impl<'a> StatefulWidget for List<'a> { impl<'a> Widget for List<'a> { fn render(self, area: Rect, buf: &mut Buffer) { let mut state = ListState::default(); - StatefulWidget::render(self, area, buf, &mut state); + Self::render_list(self, area, buf, &mut state); } } diff --git a/helix-tui/src/widgets/mod.rs b/helix-tui/src/widgets/mod.rs index 3a0dfc5d8a0c..7145ec678cb7 100644 --- a/helix-tui/src/widgets/mod.rs +++ b/helix-tui/src/widgets/mod.rs @@ -10,13 +10,13 @@ //! - [`Paragraph`] mod block; -// mod list; +mod list; mod paragraph; mod reflow; mod table; pub use self::block::{Block, BorderType}; -// pub use self::list::{List, ListItem, ListState}; +pub use self::list::{List, ListItem, ListState}; pub use self::paragraph::{Paragraph, Wrap}; pub use self::table::{Cell, Row, Table, TableState}; diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index bcee1a0a7a71..fb3c1608472e 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -11,8 +11,8 @@ repository.workspace = true homepage.workspace = true [features] -default = [] term = ["crossterm"] +steel = ["dep:steel-core", "helix-core/steel"] unicode-lines = [] [dependencies] @@ -49,6 +49,9 @@ serde_json = "1.0" toml = "0.8" log = "~0.4" +# plugin support +steel-core = { workspace = true, optional = true } + parking_lot.workspace = true thiserror.workspace = true diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 41c9ee1ef6e4..327cf7c26bc3 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -200,6 +200,8 @@ pub struct Document { // when document was used for most-recent-used buffer picker pub focused_at: std::time::Instant, + // A name separate from the file name + pub name: Option, pub readonly: bool, /// Annotations for LSP document color swatches @@ -715,6 +717,7 @@ impl Document { config, version_control_head: None, focused_at: std::time::Instant::now(), + name: None, readonly: false, jump_labels: HashMap::new(), color_swatches: None, @@ -1958,7 +1961,9 @@ impl Document { pub fn display_name(&self) -> Cow<'_, str> { self.relative_path() - .map_or_else(|| SCRATCH_BUFFER_NAME.into(), |path| path.to_string_lossy()) + .map(|path| path.to_string_lossy().to_string().into()) + .or_else(|| self.name.as_ref().map(|x| Cow::Owned(x.clone()))) + .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into()) } // transact(Fn) ? diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index dfade86baf01..4819bb5c33b7 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -2,9 +2,10 @@ use crate::{ annotations::diagnostics::{DiagnosticFilter, InlineDiagnosticsConfig}, clipboard::ClipboardProvider, document::{ - DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint, + DocumentOpenError, DocumentSavedEvent, DocumentSavedEventFuture, DocumentSavedEventResult, + Mode, SavePoint, }, - events::{DocumentDidClose, DocumentDidOpen, DocumentFocusLost}, + events::{DocumentDidClose, DocumentDidOpen, DocumentFocusLost, DocumentSaved}, graphics::{CursorKind, Rect}, handlers::Handlers, info::Info, @@ -1119,6 +1120,17 @@ pub struct Editor { pub mouse_down_range: Option, pub cursor_cache: CursorCache, + + pub editor_clipping: ClippingConfiguration, + pub user_defined_themes: HashMap, +} + +#[derive(Default)] +pub struct ClippingConfiguration { + pub top: Option, + pub bottom: Option, + pub left: Option, + pub right: Option, } pub type Motion = Box; @@ -1137,6 +1149,7 @@ pub enum EditorEvent { pub enum ConfigEvent { Refresh, Update(Box), + Change, } enum ThemeAction { @@ -1241,6 +1254,8 @@ impl Editor { handlers, mouse_down_range: None, cursor_cache: CursorCache::default(), + editor_clipping: ClippingConfiguration::default(), + user_defined_themes: Default::default(), } } @@ -1602,6 +1617,8 @@ impl Editor { pub fn switch(&mut self, id: DocumentId, action: Action) { use crate::tree::Layout; + log::info!("Switching view: {:?}", id); + if !self.documents.contains_key(&id) { log::error!("cannot switch to document that does not exist (anymore)"); return; @@ -1915,6 +1932,11 @@ impl Editor { self.write_count += 1; + dispatch(DocumentSaved { + editor: self, + doc: doc_id, + }); + Ok(()) } @@ -1930,6 +1952,8 @@ impl Editor { // if leaving the view: mode should reset and the cursor should be // within view if prev_id != view_id { + // TODO: Consult map for modes to change given file type? + self.enter_normal_mode(); self.ensure_cursor_in_view(view_id); diff --git a/helix-view/src/events.rs b/helix-view/src/events.rs index 4a44beb3540c..d32652443221 100644 --- a/helix-view/src/events.rs +++ b/helix-view/src/events.rs @@ -24,6 +24,7 @@ events! { DiagnosticsDidChange<'a> { editor: &'a mut Editor, doc: DocumentId } // called **after** a document loses focus (but not when its closed) DocumentFocusLost<'a> { editor: &'a mut Editor, doc: DocumentId } + DocumentSaved<'a> { editor: &'a mut Editor, doc: DocumentId } LanguageServerInitialized<'a> { editor: &'a mut Editor, diff --git a/helix-view/src/extension.rs b/helix-view/src/extension.rs new file mode 100644 index 000000000000..32cba948be2e --- /dev/null +++ b/helix-view/src/extension.rs @@ -0,0 +1,75 @@ +use crate::DocumentId; + +pub fn document_id_to_usize(doc_id: &DocumentId) -> usize { + doc_id.0.into() +} + +#[cfg(feature = "steel")] +mod steel_implementations { + + use steel::{ + gc::unsafe_erased_pointers::CustomReference, + rvals::{as_underlying_type, Custom}, + }; + + use crate::{ + document::Mode, + editor::{ + Action, AutoSave, BufferLine, CursorShapeConfig, FilePickerConfig, GutterConfig, + IndentGuidesConfig, LineEndingConfig, LineNumber, LspConfig, SearchConfig, + SmartTabConfig, StatusLineConfig, TerminalConfig, WhitespaceConfig, + }, + graphics::{Color, Rect, Style, UnderlineStyle}, + input::Event, + Document, DocumentId, Editor, ViewId, + }; + + impl steel::gc::unsafe_erased_pointers::CustomReference for Editor {} + steel::custom_reference!(Editor); + + impl steel::rvals::Custom for Mode {} + impl steel::rvals::Custom for Event {} + impl Custom for Style { + fn fmt(&self) -> Option> { + Some(Ok(format!("{:?}", self))) + } + } + impl Custom for Color { + fn fmt(&self) -> Option> { + Some(Ok(format!("{:?}", self))) + } + } + impl Custom for UnderlineStyle {} + + impl CustomReference for Event {} + impl Custom for Rect { + fn equality_hint(&self, other: &dyn steel::rvals::CustomType) -> bool { + if let Some(other) = as_underlying_type::(other) { + self == other + } else { + false + } + } + } + impl Custom for crate::graphics::CursorKind {} + impl Custom for DocumentId {} + impl Custom for ViewId {} + impl CustomReference for Document {} + + impl Custom for Action {} + + impl Custom for FilePickerConfig {} + impl Custom for StatusLineConfig {} + impl Custom for SearchConfig {} + impl Custom for TerminalConfig {} + impl Custom for WhitespaceConfig {} + impl Custom for CursorShapeConfig {} + impl Custom for BufferLine {} + impl Custom for LineNumber {} + impl Custom for GutterConfig {} + impl Custom for LspConfig {} + impl Custom for IndentGuidesConfig {} + impl Custom for LineEndingConfig {} + impl Custom for SmartTabConfig {} + impl Custom for AutoSave {} +} diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index 3cd3c8626524..c22d873f325a 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -317,6 +317,32 @@ impl From for crossterm::style::Color { } } +impl Color { + pub fn red(&self) -> Option { + if let Self::Rgb(r, _, _) = self { + Some(*r) + } else { + None + } + } + + pub fn green(&self) -> Option { + if let Self::Rgb(_, g, _) = self { + Some(*g) + } else { + None + } + } + + pub fn blue(&self) -> Option { + if let Self::Rgb(_, _, b) = self { + Some(*b) + } else { + None + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum UnderlineStyle { Reset, diff --git a/helix-view/src/info.rs b/helix-view/src/info.rs index d1e90b5a0373..c6ae2a0b5798 100644 --- a/helix-view/src/info.rs +++ b/helix-view/src/info.rs @@ -39,19 +39,31 @@ impl Info { .unwrap(); let mut text = String::new(); + let mut height = 0; + for (item, desc) in body { - let _ = writeln!( - text, - "{:width$} {}", - item.as_ref(), - desc.as_ref(), - width = item_width - ); + let mut line_iter = desc.as_ref().lines(); + + if let Some(first_line) = line_iter.next() { + let _ = writeln!( + text, + "{:width$} {}", + item.as_ref(), + first_line, + width = item_width + ); + height += 1; + } + + for line in line_iter { + let _ = writeln!(text, "{:width$} {}", "", line, width = item_width); + height += 1; + } } Self { title, - width: text.lines().map(|l| l.width()).max().unwrap() as u16, + width: text.lines().map(|l| l.width()).max().unwrap_or(body.len()) as u16, height: body.len() as u16, text, } diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index e30a233816da..e71326471e3e 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -19,6 +19,8 @@ pub mod theme; pub mod tree; pub mod view; +pub mod extension; + use std::num::NonZeroUsize; // uses NonZeroUsize so Option use a byte rather than two diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index af8f03bca050..93030b3d0f22 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -337,10 +337,27 @@ impl Theme { &self.name } + pub fn set_name(&mut self, name: String) { + self.name = name; + } + pub fn get(&self, scope: &str) -> Style { self.try_get(scope).unwrap_or_default() } + pub fn set(&mut self, scope: String, style: Style) { + if self.styles.insert(scope.to_string(), style).is_some() { + for (name, highlights) in self.scopes.iter().zip(self.highlights.iter_mut()) { + if *name == scope { + *highlights = style; + } + } + } else { + self.scopes.push(scope); + self.highlights.push(style); + } + } + /// Get the style of a scope, falling back to dot separated broader /// scopes. For example if `ui.text.focus` is not defined in the theme, /// `ui.text` is tried and then `ui` is tried. @@ -386,7 +403,7 @@ impl Theme { }) } - fn from_toml(value: Value) -> (Self, Vec) { + pub fn from_toml(value: Value) -> (Self, Vec) { if let Value::Table(table) = value { Theme::from_keys(table) } else { @@ -408,7 +425,7 @@ impl Theme { } } -struct ThemePalette { +pub struct ThemePalette { palette: HashMap, } diff --git a/helix-view/src/tree.rs b/helix-view/src/tree.rs index aba947a218cc..56a6c2dd51a7 100644 --- a/helix-view/src/tree.rs +++ b/helix-view/src/tree.rs @@ -669,6 +669,13 @@ impl Tree { pub fn area(&self) -> Rect { self.area } + + pub fn view_id_area(&self, id: ViewId) -> Option { + self.nodes.get(id).map(|node| match &node.content { + Content::View(v) => v.area, + Content::Container(c) => c.area, + }) + } } #[derive(Debug)] diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 70e85c40b4af..2d52d6a4b950 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.76.0" +channel = "1.81.0" components = ["rustfmt", "rust-src", "clippy"] diff --git a/steel b/steel new file mode 160000 index 000000000000..a93900c4f44c --- /dev/null +++ b/steel @@ -0,0 +1 @@ +Subproject commit a93900c4f44cd2b9bc065b63c867a8973620f113 diff --git a/xtask/src/codegen.rs b/xtask/src/codegen.rs new file mode 100644 index 000000000000..470cbf8f101f --- /dev/null +++ b/xtask/src/codegen.rs @@ -0,0 +1,5 @@ +use helix_term::commands::ScriptingEngine; + +pub fn code_gen() { + ScriptingEngine::generate_sources() +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index c7440fcc1888..64ad74f88e0c 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,3 +1,4 @@ +mod codegen; mod docgen; mod helpers; mod path; @@ -7,8 +8,11 @@ use std::{env, error::Error}; type DynError = Box; pub mod tasks { + use crate::codegen::code_gen; use crate::DynError; + use std::path::{Path, PathBuf}; + pub fn docgen() -> Result<(), DynError> { use crate::docgen::*; write(TYPABLE_COMMANDS_MD_OUTPUT, &typable_commands()?); @@ -55,6 +59,73 @@ pub mod tasks { Ok(()) } + pub fn codegen() { + code_gen() + } + + pub fn install_steel() { + fn workspace_dir() -> PathBuf { + let output = std::process::Command::new(env!("CARGO")) + .arg("locate-project") + .arg("--workspace") + .arg("--message-format=plain") + .output() + .unwrap() + .stdout; + let cargo_path = Path::new(std::str::from_utf8(&output).unwrap().trim()); + cargo_path.parent().unwrap().to_path_buf() + } + + // Update the steel submodule + std::process::Command::new("git") + .args(["submodule", "init"]) + .spawn() + .unwrap() + .wait() + .unwrap(); + + std::process::Command::new("git") + .args(["submodule", "foreach", "git", "pull", "origin", "master"]) + .spawn() + .unwrap() + .wait() + .unwrap(); + + let mut workspace_dir = workspace_dir(); + + workspace_dir.push("steel"); + + std::process::Command::new("cargo") + .args(["xtask", "install"]) + .current_dir(workspace_dir) + .spawn() + .unwrap() + .wait() + .unwrap(); + + println!("=> Finished installing steel"); + + code_gen(); + + let helix_scm_path = helix_term::commands::helix_module_file(); + let steel_init_path = helix_term::commands::steel_init_file(); + + if !helix_scm_path.exists() { + std::fs::File::create(helix_scm_path).expect("Unable to create new helix.scm file!"); + } + + if !steel_init_path.exists() { + std::fs::File::create(steel_init_path).expect("Unable to create new init.scm file!"); + } + + std::process::Command::new("cargo") + .args(["install", "--path", "helix-term", "--locked", "--force"]) + .spawn() + .unwrap() + .wait() + .unwrap(); + } + pub fn themecheck() -> Result<(), DynError> { use helix_view::theme::Loader; @@ -95,6 +166,8 @@ Usage: Run with `cargo xtask `, eg. `cargo xtask docgen`. Tasks: docgen: Generate files to be included in the mdbook output. query-check: Check that tree-sitter queries are valid. + code-gen: Generate files associated with steel + steel: Install steel theme-check: Check that theme files in runtime/themes are valid. " ); @@ -108,6 +181,8 @@ fn main() -> Result<(), DynError> { Some(t) => match t.as_str() { "docgen" => tasks::docgen()?, "query-check" => tasks::querycheck()?, + "code-gen" => tasks::codegen(), + "steel" => tasks::install_steel(), "theme-check" => tasks::themecheck()?, invalid => return Err(format!("Invalid task name: {}", invalid).into()), },