diff --git a/Cargo.lock b/Cargo.lock index 0d781a62bd9f..87c91a6ddbaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,54 @@ # It is not intended for manual editing. version = 3 +[[package]] +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 = "d7178468b407a4ee10e881bc7a328a65e739f0863615cca4429d43916b05e898" +dependencies = [ + "abi_stable_shared", + "as_derive_utils", + "core_extensions", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", + "typed-arena", +] + +[[package]] +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 = "ahash" version = "0.8.12" @@ -9,7 +57,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom 0.3.1", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -17,18 +65,18 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[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_system_properties" @@ -45,17 +93,90 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "anymap3" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170433209e817da6aae2c51aa0dd443009a613425dd041ebfb2492d1c4c11a25" + +[[package]] +name = "ar_archive_writer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" +dependencies = [ + "object", +] + [[package]] name = "arc-swap" version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "archery" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae2ed21cd55021f05707a807a5fc85695dafb98832921f6cfa06db67ca5b869" +dependencies = [ + "triomphe", +] + +[[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.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bigdecimal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "560f42649de9fa436b73517378a147ec21f6c997a546581df4b4b31677828934" +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 = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] [[package]] name = "bitflags" @@ -63,6 +184,21 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + +[[package]] +name = "bitmaps" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" + [[package]] name = "block-buffer" version = "0.10.4" @@ -74,9 +210,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "regex-automata", @@ -107,11 +243,20 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" -version = "1.2.44" +version = "1.2.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" +checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" dependencies = [ "find-msvc-tools", "shlex", @@ -119,9 +264,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chardetng" @@ -160,6 +305,58 @@ 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.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" +dependencies = [ + "typewit", +] + [[package]] name = "content_inspector" version = "0.2.4" @@ -169,12 +366,45 @@ dependencies = [ "memchr", ] +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "coolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "980c2afde4af43d6a05c5be738f9eae595cff86dce1f38f88b95058a98c027f3" +dependencies = [ + "crossterm 0.29.0", +] + [[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42bb5e5d0269fd4f739ea6cedaf29c16d81c27a7ce7582008e90eb50dcd57003" +dependencies = [ + "core_extensions_proc_macros", +] + +[[package]] +name = "core_extensions_proc_macros" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533d38ecd2709b7608fb8e18e4504deb99e9a72879e6aa66373a76d8dc4259ea" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -193,11 +423,59 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crokey" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51360853ebbeb3df20c76c82aecf43d387a62860f1a59ba65ab51f00eea85aad" +dependencies = [ + "crokey-proc_macros", + "crossterm 0.29.0", + "once_cell", + "serde", + "strict", +] + +[[package]] +name = "crokey-proc_macros" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf1a727caeb5ee5e0a0826a97f205a9cf84ee964b0b48239fef5214a00ae439" +dependencies = [ + "crossterm 0.29.0", + "proc-macro2", + "quote", + "strict", + "syn 2.0.110", +] + +[[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.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +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", @@ -212,11 +490,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" @@ -235,6 +522,24 @@ dependencies = [ "winapi", ] +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix 1.1.2", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -246,9 +551,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -268,6 +573,27 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "digest" version = "0.10.7" @@ -286,7 +612,16 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", ] [[package]] @@ -297,9 +632,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,25 +662,25 @@ 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" -version = "0.3.10" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "error-code" -version = "3.2.0" +version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" [[package]] name = "etcetera" @@ -370,9 +705,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 = "faststr" @@ -397,14 +732,14 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.25" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -433,9 +768,9 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -457,6 +792,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.110", +] + [[package]] name = "futures-task" version = "0.3.31" @@ -470,12 +816,22 @@ 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 = "generational-arena" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877e94aff08e743b651baaea359664321055749b398adff8740a7399af7796e7" +dependencies = [ + "cfg-if", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -486,27 +842,40 @@ dependencies = [ "version_check", ] +[[package]] +name = "generic_singleton" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2d5de0fc83987dac514f3b910c5d08392b220efe8cf72086c660029a197bf73" +dependencies = [ + "anymap3", + "lazy_static", + "parking_lot", +] + [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets 0.52.6", + "r-efi", + "wasip2", + "wasm-bindgen", ] [[package]] @@ -1254,6 +1623,12 @@ dependencies = [ "gix-validate", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "globset" version = "0.4.16" @@ -1315,9 +1690,20 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", + "serde", +] [[package]] name = "hashbrown" @@ -1380,6 +1766,7 @@ dependencies = [ "slotmap", "smallvec", "smartstring", + "steel-core", "textwrap", "toml", "tree-house", @@ -1504,10 +1891,11 @@ dependencies = [ "arc-swap", "chrono", "content_inspector", - "crossterm", + "crossterm 0.28.1", "dashmap", "fern", "futures-util", + "globset", "grep-matcher", "grep-regex", "grep-searcher", @@ -1521,7 +1909,7 @@ dependencies = [ "helix-vcs", "helix-view", "ignore", - "indexmap", + "indexmap 2.12.0", "indoc", "libc", "log", @@ -1535,6 +1923,8 @@ dependencies = [ "signal-hook", "signal-hook-tokio", "smallvec", + "steel-core", + "steel-doc", "tempfile", "termina", "termini", @@ -1551,11 +1941,12 @@ version = "25.7.1" dependencies = [ "bitflags", "cassowary", - "crossterm", + "crossterm 0.28.1", "helix-core", "helix-view", "log", "once_cell", + "steel-core", "termina", "termini", "unicode-segmentation", @@ -1586,7 +1977,7 @@ dependencies = [ "bitflags", "chardetng", "clipboard-win", - "crossterm", + "crossterm 0.28.1", "futures-util", "helix-core", "helix-dap", @@ -1605,6 +1996,7 @@ dependencies = [ "serde", "serde_json", "slotmap", + "steel-core", "tempfile", "termina", "thiserror", @@ -1616,29 +2008,36 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[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 = "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.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core", ] @@ -1653,128 +2052,117 @@ dependencies = [ ] [[package]] -name = "icu_collections" -version = "1.5.0" +name = "icu_casemap" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "d4ca9983e8bf51223c2f89014fa4eaa9e9b336c47f3af0d000538f86f841fba1" dependencies = [ - "displaydoc", - "yoke", - "zerofrom", + "icu_casemap_data", + "icu_collections", + "icu_locale_core", + "icu_properties", + "icu_provider", + "potential_utf", + "writeable", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_casemap_data" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "98d4663d0f99b301033a19e0acf94e9d2fa4b107638580165e5a6ccc49ad1450" + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", - "litemap", - "tinystr", - "writeable", + "potential_utf", + "serde", + "yoke", + "zerofrom", "zerovec", ] [[package]] -name = "icu_locid_transform" -version = "1.5.0" +name = "icu_locale_core" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", + "litemap", + "serde", "tinystr", + "writeable", "zerovec", ] -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", + "icu_locale_core", + "serde", "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1783,9 +2171,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -1807,6 +2195,46 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "im" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +dependencies = [ + "bitmaps 2.1.0", + "rand_core 0.6.4", + "rand_xoshiro 0.6.0", + "serde", + "sized-chunks", + "typenum", + "version_check", +] + +[[package]] +name = "im-lists" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "952b35313cb20214b270aa12e6d144270cc1c44d29caf207565f2cbeb926db9d" +dependencies = [ + "generic_singleton", + "smallvec", +] + +[[package]] +name = "im-rc" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe" +dependencies = [ + "bitmaps 2.1.0", + "rand_core 0.6.4", + "rand_xoshiro 0.6.0", + "serde", + "sized-chunks", + "typenum", + "version_check", +] + [[package]] name = "imara-diff" version = "0.1.8" @@ -1826,6 +2254,40 @@ dependencies = [ "memchr", ] +[[package]] +name = "imbl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33afdc5d333c1a43f1f640bfc6ad3788729e5b2f18472e5d33a9187315257f8e" +dependencies = [ + "archery", + "bitmaps 3.2.1", + "imbl-sized-chunks", + "rand_core 0.9.3", + "rand_xoshiro 0.7.0", + "serde", + "version_check", +] + +[[package]] +name = "imbl-sized-chunks" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4241005618a62f8d57b2febd02510fb96e0137304728543dfc5fd6f052c22d" +dependencies = [ + "bitmaps 3.2.1", +] + +[[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.12.0" @@ -1866,34 +2328,34 @@ 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.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" dependencies = [ "jiff-static", "jiff-tzdb-platform", "log", "portable-atomic", "portable-atomic-util", - "serde", - "windows-sys 0.59.0", + "serde_core", + "windows-sys 0.61.2", ] [[package]] name = "jiff-static" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -1913,9 +2375,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.80" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -1930,6 +2392,47 @@ 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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "191898e17ddee19e60bccb3945aa02339e81edd4a8c50e21fd4d48cdecda7b29" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c35dc8b0da83d1a9507e12122c80dea71a9c7c613014347392483a83ea593e04" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.110", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.177" @@ -1938,19 +2441,35 @@ checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libloading" -version = "0.8.7" +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.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.53.2", + "windows-link", ] +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags", "libc", @@ -1980,9 +2499,15 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.7.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" @@ -2007,14 +2532,24 @@ checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", ] [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memmap2" @@ -2025,37 +2560,45 @@ dependencies = [ "libc", ] +[[package]] +name = "minimad" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c5d708226d186590a7b6d4a9780e2bdda5f689e0d58cd17012a298efd745d2" +dependencies = [ + "once_cell", +] + [[package]] name = "mio" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ - "hermit-abi", "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] name = "munge" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7feb0b48aa0a25f9fe0899482c6e1379ee7a11b24a53073eacdecb9adb6dc60" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" dependencies = [ "munge_macro", ] [[package]] name = "munge_macro" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2e3795a5d2da581a8b252fec6022eee01aea10161a4d1bf237d4cbe47f7e988" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -2079,6 +2622,37 @@ dependencies = [ "unicode-segmentation", ] +[[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-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "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", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2090,14 +2664,23 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ "hermit-abi", "libc", ] +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -2138,23 +2721,29 @@ dependencies = [ "windows-link", ] +[[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" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[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" @@ -2162,11 +2751,25 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.2", + "windows-sys 0.61.2", +] + [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" @@ -2177,11 +2780,42 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "serde_core", + "writeable", + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d22152487193190344590e4f30e219cf3fe140d9e7a3fdb683d82aa2c5f4156" +dependencies = [ + "arrayvec", + "typed-arena", + "unicode-width 0.2.2", +] + [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -2195,24 +2829,34 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "psm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" +dependencies = [ + "ar_archive_writer", + "cc", +] + [[package]] name = "ptr_meta" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9e76f66d3f9606f44e45598d155cb13ecf09f4a28199e48daf8c8fc937ea90" +checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" dependencies = [ "ptr_meta_derive", ] [[package]] name = "ptr_meta_derive" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca414edb151b4c8d125c12566ab0d74dc9cdba36fb80eb7b848c15f495fd32d1" +checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -2232,23 +2876,29 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ - "rand", + "rand 0.8.5", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rancor" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf5f7161924b9d1cea0e4cabc97c372cea92b5f927fc13c6bca67157a0ad947" +checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" dependencies = [ "ptr_meta", ] @@ -2257,25 +2907,72 @@ dependencies = [ name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core 0.9.3", +] + +[[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]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" dependencies = [ - "rand_core", + "rand_core 0.6.4", ] [[package]] -name = "rand_core" -version = "0.6.4" +name = "rand_xoshiro" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" dependencies = [ - "getrandom 0.2.15", + "rand_core 0.9.3", ] [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -2283,9 +2980,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -2293,31 +2990,31 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.3" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "ref-cast" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -2358,25 +3055,34 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "rend" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35e8a6bf28cd121053a66aa2e6a2e3eaffad4a60012179f0e864aa5ffeff215" +checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" + +[[package]] +name = "repr_offset" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1070755bd29dffc19d0971cab794e607839ba2ef4b69a9e6fbc8733c1b72ea" +dependencies = [ + "tstr", +] [[package]] name = "rkyv" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f5c3e5da784cd8c69d32cdc84673f3204536ca56e1fa01be31a74b92c932ac" +checksum = "35a640b26f007713818e9a9b65d34da1cf58538207b052916a83d80e43f3ffa4" dependencies = [ "bytes", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.12.0", "munge", "ptr_meta", "rancor", @@ -2388,13 +3094,13 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4270433626cffc9c4c1d3707dd681f2a2718d3d7b09ad754bec137acecda8d22" +checksum = "bd83f5f173ff41e00337d97f6572e416d022ef8a19f371817259ae960324c482" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -2407,6 +3113,21 @@ dependencies = [ "str_indices", ] +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[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" @@ -2441,9 +3162,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[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" @@ -2460,6 +3181,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -2487,7 +3214,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -2533,6 +3260,15 @@ dependencies = [ "sha1", ] +[[package]] +name = "shared_vector" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fedf3f42342bc599d499101f718082efc026b6df8fb1ea7e6ea2ae19bf389cfd" +dependencies = [ + "allocator-api2", +] + [[package]] name = "shell-words" version = "1.1.0" @@ -2557,9 +3293,9 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio", @@ -2568,9 +3304,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] @@ -2594,14 +3330,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] -name = "slab" -version = "0.4.9" +name = "sized-chunks" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" dependencies = [ - "autocfg", + "bitmaps 2.1.0", + "typenum", ] +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + [[package]] name = "slotmap" version = "1.0.7" @@ -2636,12 +3379,12 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2655,9 +3398,9 @@ dependencies = [ [[package]] name = "sonic-rs" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22540d56ba14521e4878ad436d498518c59698c39a89d5905c694932f0bf7134" +checksum = "4425ea8d66ec950e0a8f2ef52c766cc3d68d661d9a0845c353c40833179fd866" dependencies = [ "ahash", "bumpalo", @@ -2676,18 +3419,31 @@ dependencies = [ [[package]] name = "sonic-simd" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b421f7b6aa4a5de8f685aaf398dfaa828346ee639d2b1c1061ab43d40baa6223" +checksum = "5707edbfb34a40c9f2a55fa09a49101d9fec4e0cc171ce386086bd9616f34257" dependencies = [ "cfg-if", ] [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stacker" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] [[package]] name = "static_assertions" @@ -2695,17 +3451,152 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "steel-core" +version = "0.7.0" +source = "git+https://github.com/mattwparas/steel.git#3a5fccea2bcccf75a6a37bdfcdfda058d18c0db3" +dependencies = [ + "abi_stable", + "anyhow", + "arc-swap", + "async-ffi", + "bigdecimal", + "bincode", + "chrono", + "codespan-reporting", + "compact_str", + "crossbeam-channel", + "crossbeam-queue", + "crossbeam-utils", + "env_home", + "futures-executor", + "futures-task", + "futures-util", + "getrandom 0.3.4", + "glob", + "httparse", + "icu_casemap", + "im", + "im-lists", + "im-rc", + "imbl", + "lasso", + "log", + "md-5", + "num-bigint", + "num-integer", + "num-rational", + "num-traits", + "once_cell", + "parking_lot", + "polling", + "rand 0.9.2", + "rustc-hash", + "serde", + "serde_json", + "shared_vector", + "smallvec", + "stacker", + "steel-derive", + "steel-gen", + "steel-parser", + "steel-quickscope", + "strsim", + "termimad", + "triomphe", + "weak-table", + "which", + "xdg", +] + +[[package]] +name = "steel-derive" +version = "0.6.0" +source = "git+https://github.com/mattwparas/steel.git#3a5fccea2bcccf75a6a37bdfcdfda058d18c0db3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "steel-doc" +version = "0.7.0" +source = "git+https://github.com/mattwparas/steel.git#3a5fccea2bcccf75a6a37bdfcdfda058d18c0db3" +dependencies = [ + "steel-core", +] + +[[package]] +name = "steel-gen" +version = "0.3.0" +source = "git+https://github.com/mattwparas/steel.git#3a5fccea2bcccf75a6a37bdfcdfda058d18c0db3" +dependencies = [ + "codegen", + "serde", +] + +[[package]] +name = "steel-parser" +version = "0.7.0" +source = "git+https://github.com/mattwparas/steel.git#3a5fccea2bcccf75a6a37bdfcdfda058d18c0db3" +dependencies = [ + "compact_str", + "lasso", + "log", + "num-bigint", + "num-rational", + "num-traits", + "once_cell", + "pretty", + "rustc-hash", + "serde", + "smallvec", +] + +[[package]] +name = "steel-quickscope" +version = "0.2.0" +source = "git+https://github.com/mattwparas/steel.git#3a5fccea2bcccf75a6a37bdfcdfda058d18c0db3" +dependencies = [ + "indexmap 2.12.0", + "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.101" +version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", @@ -2714,13 +3605,13 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -2730,12 +3621,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.1", + "getrandom 0.3.4", "once_cell", "rustix 1.1.2", "windows-sys 0.61.2", ] +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7301d9c2c4939c97f25376b70d3c13311f8fefdee44092fc361d2a98adc2cbb6" +dependencies = [ + "coolor", + "crokey", + "crossbeam", + "lazy-regex", + "minimad", + "serde", + "thiserror", + "unicode-width 0.1.12", +] + [[package]] name = "termina" version = "0.1.1" @@ -2767,7 +3683,7 @@ checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" dependencies = [ "smawk", "unicode-linebreak", - "unicode-width 0.2.0", + "unicode-width 0.2.2", ] [[package]] @@ -2787,7 +3703,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -2801,19 +3717,20 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] [[package]] name = "tinyvec" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -2849,7 +3766,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -2869,7 +3786,7 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ - "indexmap", + "indexmap 2.12.0", "serde_core", "serde_spanned", "toml_datetime", @@ -2921,32 +3838,66 @@ dependencies = [ [[package]] name = "tree-house-bindings" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1646788fe0afdbf8e191b5d0f558df7333d8857665a67053c532ec811e6086" +checksum = "f24f037be888a6ea5521046288c126aa700ade79e9fe7c74cdef79207281c20b" dependencies = [ "cc", - "libloading", + "libloading 0.8.9", "regex-cursor", "ropey", "thiserror", ] [[package]] -name = "typenum" -version = "1.18.0" +name = "triomphe" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" +dependencies = [ + "serde", + "stable_deref_trait", +] [[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.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "typewit" +version = "1.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c1ae7cc0fdb8b842d65d127cb981574b0d2b249b74d1c7a2986863dc134f71" + +[[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" @@ -2961,9 +3912,9 @@ checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-linebreak" @@ -2973,9 +3924,9 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] @@ -2994,15 +3945,15 @@ checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -3010,12 +3961,6 @@ dependencies = [ "serde", ] -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -3050,24 +3995,24 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.13.3+wasi-0.2.2" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.103" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", @@ -3076,25 +4021,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-macro" -version = "0.2.103" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3102,26 +4033,32 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.103" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn", - "wasm-bindgen-backend", + "syn 2.0.110", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.103" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] +[[package]] +name = "weak-table" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323f4da9523e9a669e1eaf9c6e763892769b1d38c623913647bfdc1532fe4549" + [[package]] name = "which" version = "8.0.0" @@ -3151,11 +4088,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3166,11 +4103,37 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.52.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-targets 0.52.6", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", ] [[package]] @@ -3180,12 +4143,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows-sys" -version = "0.52.0" +name = "windows-result" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-targets 0.52.6", + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", ] [[package]] @@ -3197,6 +4169,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -3224,18 +4205,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -3246,9 +4228,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -3258,9 +4240,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -3270,9 +4252,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -3282,9 +4264,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -3294,9 +4276,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -3306,9 +4288,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -3318,9 +4300,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -3330,9 +4312,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" @@ -3350,25 +4332,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] -name = "wit-bindgen-rt" -version = "0.33.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" -dependencies = [ - "bitflags", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] -name = "write16" -version = "1.0.0" +name = "writeable" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] -name = "writeable" -version = "0.5.5" +name = "xdg" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" [[package]] name = "xtask" @@ -3383,11 +4362,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.4" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -3395,13 +4373,13 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.4" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", "synstructure", ] @@ -3422,36 +4400,48 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[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.110", "synstructure", ] +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", @@ -3459,13 +4449,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 90542831f891..c9d79853b2db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ package.helix-term.opt-level = 2 tree-house = { version = "0.3.0", default-features = false } nucleo = "0.5.0" slotmap = "1.0.7" +steel-core = { git = "https://github.com/mattwparas/steel.git", version = "0.7.0", features = ["anyhow", "dylibs", "sync", "triomphe", "imbl"] } thiserror = "2.0" tempfile = "3.23.0" bitflags = "2.10" diff --git a/STEEL.md b/STEEL.md new file mode 100644 index 000000000000..43623403269e --- /dev/null +++ b/STEEL.md @@ -0,0 +1,272 @@ +# Building + +You will need: + +* A clone of this fork, on the branch `steel-event-system` + +## 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 package manager `forge`. + +## Developing + +The easiest way to contribute would be to adjust the default features on the `helix-term` crate: + +```toml +[features] +features = ["git", "steel"] +``` + +## Setting up configurations for helix + +There are 2 important files you'll want, which should be auto generated during the installation process if they don't already exist: + +* `~/.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) + +(define (current-path) + (let* ([focus (editor-focus)] + [focus-doc-id (editor->doc-id focus)]) + (editor-document->path focus-doc-id))) + +;;@doc +;; Specialized shell implementation, where % is a wildcard for the current file +(define (shell . args) + (helix.run-shell-command + (string-join + ;; Replace the % with the current file + (map (lambda (x) (if (equal? x "%") (current-path) x)) args) + " "))) + +;;@doc +;; Adds the current file to git +(define (git-add) + (shell "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")) + +;; 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 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. + + +## Writing a plugin + +### Getting setup + +Before you start, you should make sure that your configuration for the steel lsp is wired up correctly. This will give you +access to the documentation that will help you as you write your plugin. To configure the LSP, you can add this to your +`init.scm`: + +```scheme +(require "helix/configuration.scm") +(define-lsp "steel-language-server" (command "steel-language-server") (args '())) +(define-language "scheme" + (language-servers '("steel-language-server"))) +``` + +This will give you an interactive setup that can help you run plugins as you go. I also like to evaluate commands +via the buffer, by either typing them in to the command prompt or by loading the current buffer. To load the current +buffer, you can type `:eval-buffer`, or to evaluate an individual command, you can run `:evalp` - note, in your init.scm, you +may need to add: + +```scheme +(require (only-in "helix/ext" evalp eval-buffer)) +``` + +This brings those functions to the top level scope so that you can interact with them. You may also be keen to peruse all of the steel +functions and modules available. Those can be found in `steel-docs.md`. + + +### Command API + +There are two levels of the functionality exposed to plugins. The first is simply based around +chaining builtin commands, as if you're a lightning fast human typing commands very quickly. The other level +is a bit lower, and deals directly with the component API that helix uses to draw the text editor and various +popups, like the file picker or buffer selection. + +To understand the first level, which is accessing typed commands and static commands, i.e. commands that you +typically type via `:`, or static commands, commands which are bound to keybindings, you can look at the modules: + +* helix/commands.scm +* helix/static.scm + +Every function here implicitly has access to a context, the helix context. This assumes that you're focused onto +some buffer, and any actions are assumed to be done within that context. For example, calling `vsplit` will +split the currently focused into a second, and move your focus to that window. Keeping track of that is important +to understand where your focus is. + +In general, these functions do not return anything, given that they're purely for side effects. There are some functions +that do, and they should be documented as such. The API will need to be improved to return useful things where relevant. + +### The UI + +A good rule of thumb is to not block the UI. During the execution of a steel function, the helix context is exclusively +available to that executing function. As a result, you should not have long running functions there (note - if you end +up having an infinite loop of some kind, `ctrl-c` should break you out). + +Luckily, there are a handful of ways we can accomplish more sophisticated plugins: + +* Futures +* Threads + +There are a handful of primitives that accept a future + a callback, where the callback will get executed once the future +is complete. The future will get scheduled on to the helix event loop, so the UI won't be blocked. (TODO: Document this more!) + +Another way we can accomplish this is with native threads. Steel supports native threads, which means we can spawn a function +off on to another thread to run some code. Consider the following example which won't work: + + +```scheme +(spawn-native-thread (lambda () (time/sleep-ms 1000) (theme "focus_nova"))) ;; Note, this won't work! +``` + +This appears to spawn a thread, sleep for 1 second, and then change the theme. The issue here is that this thread does not +have control over the helix context. So what we'll have to do instead, is schedule a function to be run on the main thread: + + +```scheme +(require "helix/ext.scm") +(require-builtin steel/time) + +(spawn-native-thread + (lambda () + (hx.block-on-task + (lambda () + (time/sleep-ms 1000) + (theme "focus_nova"))))) +``` + +`hx.block-on-task` will check if we're running on the main thread. If we are already, it doesn't do anything - but otherwise, +it enqueues a callback that schedules itself onto the main thread, and waits till it can acquire the helix context. The function +is then run, and the value returned back to this thread of control. + + +There is also `hx.with-context` which does a similar thing, except it does _not_ block the current thread. + +### Components + +Coming soon! diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index e7e79ea4a064..97cd314e0b9a 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" } @@ -53,6 +54,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.workspace = true diff --git a/helix-core/src/command_line.rs b/helix-core/src/command_line.rs index 8e209d6180e0..7edb82f5545a 100644 --- a/helix-core/src/command_line.rs +++ b/helix-core/src/command_line.rs @@ -766,6 +766,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 @@ -872,7 +879,7 @@ impl<'a> Args<'a> { /// Performs any validations that must be done after the input args are finished being pushed /// with `Self::push`. - fn finish(&self) -> Result<(), ParseArgsError<'a>> { + pub fn finish(&self) -> Result<(), ParseArgsError<'a>> { if !self.validate { return Ok(()); }; @@ -1123,7 +1130,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..1179d827663a --- /dev/null +++ b/helix-core/src/extensions.rs @@ -0,0 +1,679 @@ +#[cfg(feature = "steel")] +pub mod steel_implementations { + + use std::borrow::Cow; + + use regex_cursor::regex_automata::util::syntax::Config; + + use steel::{ + gc::ShareableMut, + rvals::{as_underlying_type, AsRefSteelVal, Custom, SteelString}, + steel_vm::{ + builtin::{BuiltInModule, MarkdownDoc}, + register_fn::RegisterFn, + }, + SteelVal, + }; + + use helix_stdx::rope::{Regex, RegexBuilder, RopeSliceExt}; + + use crate::{ + syntax::config::{AutoPairConfig, SoftWrap}, + Range, + }; + + 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 {} + + struct SteelRopeRegex(Regex); + + #[allow(unused)] + #[derive(Debug)] + struct RegexError(String); + + impl steel::rvals::Custom for SteelRopeRegex {} + + impl steel::rvals::Custom for RegexError { + fn fmt(&self) -> Option> { + Some(Ok(format!("{:?}", self.0))) + } + } + + impl From for RegexError { + fn from(value: String) -> Self { + Self(value) + } + } + + impl SteelRopeRegex { + fn new(re: SteelString) -> Result { + match RegexBuilder::new().syntax(Config::new()).build(re.as_str()) { + Ok(regex) => Ok(SteelRopeRegex(regex)), + Err(err) => Err(RegexError(err.to_string())), + } + } + + fn is_match(&self, haystack: SteelRopeSlice) -> bool { + match self.0.find(haystack.to_slice().regex_input()) { + Some(m) => m.start() != m.end(), + None => false, + } + } + + fn find(&self, haystack: SteelRopeSlice) -> Option { + match self.0.find(haystack.to_slice().regex_input()) { + Some(m) => { + if m.start() == m.end() { + None + } else { + haystack.slice(m.start(), m.end()).ok() + } + } + None => None, + } + } + + pub fn find_all(&self, haystack: SteelRopeSlice) -> Option> { + let matches = self.0.find_iter(haystack.to_slice().regex_input()); + let mut ret: Vec = vec![]; + for m in matches { + if m.start() == m.end() { + continue; + } + let s = haystack.clone().slice(m.start(), m.end()); + if let Ok(slice) = s { + ret.push(slice); + } + } + Some(ret) + } + + pub fn split(&self, haystack: SteelRopeSlice) -> Option> { + let matches = self.0.split(haystack.to_slice().regex_input()); + let mut ret: Vec = vec![]; + for m in matches { + if m.start == m.end { + continue; + } + let s = haystack.clone().slice(m.start, m.end); + if let Ok(slice) = s { + ret.push(slice); + } + } + Some(ret) + } + + pub fn splitn(&self, haystack: SteelRopeSlice, n: usize) -> Option> { + let matches = self.0.splitn(haystack.to_slice().regex_input(), n); + let mut ret: Vec = vec![]; + for m in matches { + if m.start == m.end { + continue; + } + let s = haystack.clone().slice(m.start, m.end); + if let Ok(slice) = s { + ret.push(slice); + } + } + Some(ret) + } + } + + impl steel::rvals::Custom for Range {} + + #[allow(unused)] + 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, + } + } + + fn fmt(&self) -> Option> { + Some(Ok(format!("#", self.to_slice()))) + } + } + + 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 insert_str(&self, char_idx: usize, text: SteelString) -> Result { + let slice = self.to_slice(); + let mut rope = ropey::Rope::from(slice); + rope.try_insert(char_idx, &text)?; + Ok(Self::new(rope)) + } + + pub fn insert_char(&self, char_idx: usize, c: char) -> Result { + let slice = self.to_slice(); + let mut rope = ropey::Rope::from(slice); + rope.try_insert_char(char_idx, c)?; + Ok(Self::new(rope)) + } + + pub fn try_line_to_char(&self, line: usize) -> Result { + self.to_slice().try_line_to_char(line).map_err(RopeyError) + } + + pub fn try_line_to_byte(&self, line: usize) -> Result { + self.to_slice().try_line_to_byte(line).map_err(RopeyError) + } + + pub fn try_char_to_line(&self, line: usize) -> Result { + self.to_slice().try_char_to_line(line).map_err(RopeyError) + } + + pub fn try_byte_to_line(&self, line: usize) -> Result { + self.to_slice().try_byte_to_line(line).map_err(RopeyError) + } + + 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(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.get_line(cursor).ok_or(RopeyError( + ropey::Error::LineIndexOutOfBounds(cursor, slice.len_lines()), + ))?; + + 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(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.get_line(cursor).ok_or(RopeyError( + ropey::Error::LineIndexOutOfBounds(cursor, slice.len_lines()), + ))?; + + 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(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(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(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(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 byte_to_char(&self, pos: usize) -> Result { + Ok(self.to_slice().try_byte_to_char(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 len_bytes(&self) -> usize { + self.to_slice().len_bytes() + } + + 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 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"); + + macro_rules! register_value { + ($name:expr, $func:expr, $doc:expr) => { + module.register_fn($name, $func); + module.register_doc($name, MarkdownDoc(Cow::Borrowed($doc))); + }; + } + + register_value!( + "Rope?", + |value: SteelVal| SteelRopeSlice::as_ref(&value).is_ok(), + "Check if the given value is a rope" + ); + + register_value!( + "string->rope", + SteelRopeSlice::from_string, + r#"Converts a string into a rope. + +```scheme +(string->rope value) -> Rope? +``` + +* value : string? + "# + ); + + register_value!( + "RopeRegex?", + |value: SteelVal| SteelRopeRegex::as_ref(&value).is_ok(), + "Check if the given value is a rope regex" + ); + register_value!( + "rope-regex", + SteelRopeRegex::new, + r#"Build a new RopeRegex? with a string + +```scheme +(rope-regex string) -> RopeRegex? +``` + +* string: string? + "# + ); + register_value!( + "rope-regex-find", + SteelRopeRegex::find, + r#"Find the first match in a given rope + +```scheme +(rope-regex-find regex rope) -> Rope? +``` + +* regex: RopeRegex? +* rope: Rope? + "# + ); + register_value!( + "rope-regex-match?", + SteelRopeRegex::is_match, + r#"Returns if a regex is matching on a given rope + +```scheme +(rope-regex->match? regex rope) -> bool? +``` + +* regex: RopeRegex? +* rope: Rope? + "# + ); + register_value!( + "rope-regex-find*", + SteelRopeRegex::find_all, + r#"Find and return all matches in a given rope + +```scheme +(rope-regex-find* regex rope) -> '(Rope?) +``` +* regex: RopeRegex? +* rope: Rope? + "# + ); + register_value!( + "rope-regex-split", + SteelRopeRegex::split, + r#"Split on the match in a given rope + +```scheme +(rope-regex-split regex rope) -> '(Rope?) +``` + +* regex: RopeRegex? +* rope: Rope? +"# + ); + register_value!( + "rope-regex-splitn", + SteelRopeRegex::splitn, + r#"Split n times on the match in a given rope, return the rest + +```scheme +(rope-regex-splitn regex rope n) -> '(Rope?) +``` + +* regex: RopeRegex? +* rope: Rope? +* n: (and positive? int?) +"# + ); + + register_value!( + "rope->slice", + SteelRopeSlice::slice, + r#"Take a slice from using character indices from the rope. +Returns a new rope value. + +```scheme +(rope->slice rope start end) -> Rope? +``` + +* rope : Rope? +* start: (and positive? int?) +* end: (and positive? int?) +"# + ); + + register_value!( + "rope-char->byte", + SteelRopeSlice::char_to_byte, + r#"Convert the character offset into a byte offset for a given rope"# + ); + + register_value!( + "rope-char->byte", + SteelRopeSlice::byte_to_char, + r#"Convert the byte offset into a character offset for a given rope"# + ); + + register_value!( + "rope-line->char", + SteelRopeSlice::try_line_to_char, + r#"Convert the given line index to a character offset for a given rope + +```scheme +(rope-line->char rope line-offset) -> int? +``` + +* rope : Rope? +* line-offset: int? + "# + ); + + register_value!( + "rope-line->byte", + SteelRopeSlice::try_line_to_byte, + r#"Convert the given line index to a byte offset for a given rope + +```scheme +(rope-line->byte rope line-offset) -> int? +``` + +* rope : Rope? +* line-offset: int? + "# + ); + + register_value!( + "rope-char->line", + SteelRopeSlice::try_char_to_line, + r#"Convert the given character offset to a line offset for a given rope + +```scheme +(rope-char->line rope char-index) -> int? +``` + +* rope : Rope? +* char-index : int? + + "# + ); + + register_value!( + "rope-byte->line", + SteelRopeSlice::try_byte_to_line, + r#"Convert the given byte offset to a line offset for a given rope + +```scheme +(rope-byte->line rope byte-index) -> int? +``` + +* rope : Rope? +* byte-index : int? + + "# + ); + + register_value!( + "rope->byte-slice", + SteelRopeSlice::byte_slice, + r#"Take a slice of this rope using byte offsets + +```scheme +(rope->byte-slice rope start end) -> Rope? +``` + +* rope: Rope? +* start: (and positive? int?) +* end: (and positive? int?) +"# + ); + + register_value!( + "rope->line", + SteelRopeSlice::line, + r#"Get the line at the given line index. Returns a rope. + +```scheme +(rope->line rope index) -> Rope? + +``` + +* rope : Rope? +* index : (and positive? int?) +"# + ); + + register_value!( + "rope->string", + SteelRopeSlice::to_string, + "Convert the given rope to a string" + ); + + register_value!( + "rope-len-chars", + SteelRopeSlice::len_chars, + "Get the length of the rope in characters" + ); + register_value!( + "rope-len-bytes", + SteelRopeSlice::len_chars, + "Get the length of the rope in bytes" + ); + + register_value!( + "rope-char-ref", + SteelRopeSlice::get_char, + "Get the character at the given index" + ); + + register_value!( + "rope-len-lines", + SteelRopeSlice::len_lines, + "Get the number of lines in the rope" + ); + + register_value!( + "rope-starts-with?", + SteelRopeSlice::starts_with, + "Check if the rope starts with a given pattern" + ); + + register_value!( + "rope-ends-with?", + SteelRopeSlice::ends_with, + "Check if the rope ends with a given pattern" + ); + + register_value!( + "rope-trim-start", + SteelRopeSlice::trim_start, + "Remove the leading whitespace from the given rope" + ); + + register_value!( + "rope-insert-string", + SteelRopeSlice::insert_str, + "Insert a string at the given index into the rope" + ); + + register_value!( + "rope-insert-char", + SteelRopeSlice::insert_char, + "Insert a character at the given index" + ); + + module + } +} diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 09865ca40456..f35e8a03b4e8 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 90d7d2549f08..ae59886adc31 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -46,6 +46,19 @@ pub struct LanguageData { rainbow_query: OnceCell>, } +impl Clone for LanguageData { + fn clone(&self) -> Self { + Self { + config: self.config.clone(), + syntax: OnceCell::new(), + indent_query: OnceCell::new(), + textobject_query: OnceCell::new(), + tag_query: OnceCell::new(), + rainbow_query: OnceCell::new(), + } + } +} + impl LanguageData { fn new(config: LanguageConfiguration) -> Self { Self { @@ -271,14 +284,14 @@ pub fn read_query(lang: &str, query_filename: &str) -> String { }) } -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct Loader { languages: Vec, languages_by_extension: HashMap, languages_by_shebang: HashMap, languages_glob_matcher: FileTypeGlobMatcher, language_server_configs: HashMap, - scopes: ArcSwap>, + scopes: Arc>>, } pub type LoaderError = globset::Error; @@ -317,7 +330,7 @@ impl Loader { languages_by_shebang, languages_glob_matcher: FileTypeGlobMatcher::new(file_type_globs)?, language_server_configs: config.language_server, - scopes: ArcSwap::from_pointee(Vec::new()), + scopes: Arc::new(ArcSwap::from_pointee(Vec::new())), }) } @@ -403,6 +416,20 @@ impl Loader { self.language_for_shebang_marker(marker) } + pub fn language_configs_mut( + &mut self, + ) -> impl Iterator> { + self.languages + .iter_mut() + .map(|language| &mut language.config) + } + + pub fn language_server_configs_mut( + &mut self, + ) -> &mut HashMap { + &mut self.language_server_configs + } + fn language_for_shebang_marker(&self, marker: RopeSlice) -> Option { let shebang: Cow = marker.into(); self.languages_by_shebang.get(shebang.as_ref()).copied() @@ -460,7 +487,7 @@ impl LanguageLoader for Loader { } } -#[derive(Debug)] +#[derive(Debug, Clone)] struct FileTypeGlob { glob: globset::Glob, language: Language, @@ -472,7 +499,7 @@ impl FileTypeGlob { } } -#[derive(Debug)] +#[derive(Debug, Clone)] struct FileTypeGlobMatcher { matcher: globset::GlobSet, file_types: Vec, diff --git a/helix-core/src/syntax/config.rs b/helix-core/src/syntax/config.rs index d2e03078a918..6e87e475c03c 100644 --- a/helix-core/src/syntax/config.rs +++ b/helix-core/src/syntax/config.rs @@ -20,7 +20,7 @@ pub struct Configuration { pub language_server: HashMap, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct LanguageConfiguration { #[serde(skip)] @@ -112,7 +112,7 @@ impl LanguageConfiguration { } } -#[derive(Debug, PartialEq, Eq, Hash)] +#[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. @@ -318,7 +318,7 @@ enum LanguageServerFeatureConfiguration { Simple(String), } -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct LanguageServerFeatures { pub name: String, pub only: HashSet, @@ -397,7 +397,7 @@ where builder.build().map(Some).map_err(serde::de::Error::custom) } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "kebab-case")] pub struct LanguageServerConfiguration { pub command: String, @@ -483,7 +483,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")] @@ -627,6 +627,6 @@ where Ok(Option::::deserialize(deserializer)?.and_then(AutoPairConfig::into)) } -fn default_timeout() -> u64 { +pub fn default_timeout() -> u64 { 20 } diff --git a/helix-core/src/test.rs b/helix-core/src/test.rs index 71c71cce2812..05fa3e60693a 100644 --- a/helix-core/src/test.rs +++ b/helix-core/src/test.rs @@ -286,10 +286,7 @@ mod test { #[test] fn print_multi_code_point_grapheme() { assert_eq!( - ( - String::from("hello 👨‍👩‍👧‍👦 goodbye"), - Selection::single(13, 6) - ), + (String::from("hello 👨‍👩‍👧‍👦 goodbye"), Selection::single(13, 6)), print("hello #[|👨‍👩‍👧‍👦]# goodbye") ); } diff --git a/helix-event/Cargo.toml b/helix-event/Cargo.toml index ddf30b7d43b8..22b83397f79b 100644 --- a/helix-event/Cargo.toml +++ b/helix-event/Cargo.toml @@ -17,7 +17,7 @@ hashbrown = "0.16" 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 +# as it essentially makes the lock entirely free as long as there is no writes parking_lot = { workspace = true, features = ["hardware-lock-elision"] } once_cell = "1.21" diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 6156be9a956a..063b9796c5e2 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -32,3 +32,6 @@ arc-swap = "1" slotmap.workspace = true thiserror.workspace = true sonic-rs.workspace = true + +[features] +steel = [] diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index ebc619e24c88..3b54c989dd6d 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -1585,4 +1585,91 @@ impl Client { changes, }) } + + // Everything below is explicitly extensions used for handling non standard lsp commands + #[cfg(feature = "steel")] + pub fn non_standard_extension( + &self, + method_name: String, + params: Option, + ) -> Option>> { + Some(self.call_non_standard(DynamicLspRequest { + method_name, + params, + })) + } + + #[cfg(feature = "steel")] + fn call_non_standard(&self, request: DynamicLspRequest) -> impl Future> { + self.call_non_standard_with_timeout(request, self.req_timeout) + } + + #[cfg(feature = "steel")] + 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 rx = serde_json::to_value(&request.params) + .map_err(Error::from) + .and_then(|params| { + let request = jsonrpc::MethodCall { + jsonrpc: Some(jsonrpc::Version::V2), + id: id.clone(), + method: request.method_name, + params: Self::value_into_params(params), + }; + + let (tx, rx) = channel::>(1); + + server_tx + .send(Payload::Request { + chan: tx, + value: request, + }) + .map_err(|e| Error::Other(e.into()))?; + Ok(rx) + }); + + async move { + use std::time::Duration; + use tokio::time::timeout; + + // 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)? + } + } + + /// Send a custom RPC notification with arbitrary method and params to the language server. + #[cfg(feature = "steel")] + pub fn send_custom_notification(&self, method: String, params: Option) -> Result<()> { + let server_tx = self.server_tx.clone(); + + let params = params.unwrap_or(Value::Null); + + let notification = jsonrpc::Notification { + jsonrpc: Some(jsonrpc::Version::V2), + method, + params: Self::value_into_params(params), + }; + + server_tx + .send(Payload::Notification(notification)) + .map_err(|e| Error::Other(e.into()))?; + + Ok(()) + } +} + +#[cfg(feature = "steel")] +#[derive(serde::Serialize, Deserialize)] +pub struct DynamicLspRequest { + method_name: String, + params: Option, } diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index d01cd3995a69..f207cc5b3298 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -475,6 +475,8 @@ pub enum MethodCall { RegisterCapability(lsp::RegistrationParams), UnregisterCapability(lsp::UnregistrationParams), ShowDocument(lsp::ShowDocumentParams), + // Other kind specifically for extensions + Other(String, jsonrpc::Params), WorkspaceDiagnosticRefresh, } @@ -508,9 +510,7 @@ impl MethodCall { Self::ShowDocument(params) } lsp::request::WorkspaceDiagnosticRefresh::METHOD => Self::WorkspaceDiagnosticRefresh, - _ => { - return Err(Error::Unhandled); - } + _ => Self::Other(method.to_owned(), params), }; Ok(request) } @@ -526,6 +526,8 @@ pub enum Notification { ShowMessage(lsp::ShowMessageParams), LogMessage(lsp::LogMessageParams), ProgressMessage(lsp::ProgressParams), + // Other kind specifically for extensions + Other(String, jsonrpc::Params), } impl Notification { @@ -552,9 +554,7 @@ impl Notification { let params: lsp::ProgressParams = params.parse()?; Self::ProgressMessage(params) } - _ => { - return Err(Error::Unhandled); - } + _ => Self::Other(method.to_owned(), params), }; Ok(notification) diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index a62b2e166e3a..f37e0d461820 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -31,10 +31,11 @@ assets = [ ] [features] -default = ["git"] +default = ["git"] # Add steel here for development unicode-lines = ["helix-core/unicode-lines", "helix-view/unicode-lines"] integration = ["helix-event/integration_test"] git = ["helix-vcs/git"] +steel = ["dep:steel-core", "dep:steel-doc", "helix-core/steel", "helix-view/steel", "tui/steel", "helix-lsp/steel"] [[bin]] name = "hx" @@ -91,6 +92,11 @@ toml.workspace = true serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } +# plugin support +steel-core = { workspace = true, optional = true } +steel-doc = { git = "https://github.com/mattwparas/steel.git", version = "0.7.0", optional = true } + +globset = "0.4.16" dashmap = "6.0" [target.'cfg(windows)'.dependencies] diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index ec2dcca6d829..212e860cb050 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -2,6 +2,7 @@ use arc_swap::{access::Map, ArcSwap}; use futures_util::Stream; use helix_core::{diagnostic::Severity, pos_at_coords, syntax, Range, Selection}; use helix_lsp::{ + jsonrpc, lsp::{self, notification::Notification}, util::lsp_range_to_range, LanguageServerId, LspProgressMap, @@ -21,6 +22,7 @@ use tui::backend::Backend; use crate::{ args::Args, + commands::ScriptingEngine, compositor::{Compositor, Event}, config::Config, handlers, @@ -54,11 +56,11 @@ use tui::backend::CrosstermBackend; use tui::backend::TestBackend; #[cfg(all(not(windows), not(feature = "integration")))] -type TerminalBackend = TerminaBackend; +pub type TerminalBackend = TerminaBackend; #[cfg(all(windows, not(feature = "integration")))] -type TerminalBackend = CrosstermBackend; +pub type TerminalBackend = CrosstermBackend; #[cfg(feature = "integration")] -type TerminalBackend = TestBackend; +pub type TerminalBackend = TestBackend; #[cfg(not(windows))] type TerminalEvent = termina::Event; @@ -149,9 +151,29 @@ impl Application { &config.keys })); let editor_view = Box::new(ui::EditorView::new(Keymaps::new(keys))); + compositor.push(editor_view); - let jobs = Jobs::new(); + let mut jobs = Jobs::new(); + { + let syn_loader = editor.syn_loader.clone(); + + let mut cx = crate::commands::Context { + register: None, + count: std::num::NonZeroUsize::new(1), + editor: &mut editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs: &mut jobs, + }; + + crate::commands::ScriptingEngine::run_initialization_script( + &mut cx, + config.clone(), + syn_loader, + crate::commands::engine::TerminalEventReaderHandle::new(terminal.backend()), + ); + } if args.load_tutor { let path = helix_loader::runtime_file(Path::new("tutor")); @@ -357,6 +379,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; @@ -395,6 +421,7 @@ impl Application { }; self.config.store(Arc::new(app_config)); } + ConfigEvent::Change => {} } // Update all the relevant members in the editor after updating @@ -443,6 +470,32 @@ impl Application { self.terminal.reconfigure((&default_config.editor).into())?; // Store new config self.config.store(Arc::new(default_config)); + + { + crate::commands::ScriptingEngine::reinitialize(); + + let syn_loader = self.editor.syn_loader.clone(); + let config = self.config.clone(); + + let mut cx = crate::commands::Context { + register: None, + count: std::num::NonZeroUsize::new(1), + editor: &mut self.editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs: &mut self.jobs, + }; + + crate::commands::ScriptingEngine::run_initialization_script( + &mut cx, + config.clone(), + syn_loader, + crate::commands::engine::TerminalEventReaderHandle::new( + self.terminal.backend(), + ), + ); + } + Ok(()) }; @@ -952,6 +1005,21 @@ impl Application { // Remove the language server from the registry. self.editor.language_servers.remove_by_id(server_id); } + Notification::Other(event_name, params) => { + let mut cx = crate::compositor::Context { + editor: &mut self.editor, + scroll: None, + jobs: &mut self.jobs, + }; + + ScriptingEngine::handle_lsp_call( + &mut cx, + server_id, + event_name, + jsonrpc::Id::Null, + params, + ); + } } } Call::MethodCall(helix_lsp::jsonrpc::MethodCall { @@ -1105,6 +1173,32 @@ impl Application { let result = self.handle_show_document(params, offset_encoding); Ok(json!(result)) } + Ok(MethodCall::Other(event_name, params)) => { + let mut cx = crate::compositor::Context { + editor: &mut self.editor, + scroll: None, + jobs: &mut self.jobs, + }; + + let reply = ScriptingEngine::handle_lsp_call( + &mut cx, + server_id, + event_name, + id.clone(), + params, + ); + + if let Some(reply) = reply { + let language_server = language_server!(); + if let Err(err) = language_server.reply(id.clone(), reply) { + log::error!( + "Failed to send reply to server '{}' request {id}: {err}", + language_server.name() + ); + } + }; + return; + } Ok(MethodCall::WorkspaceDiagnosticRefresh) => { let language_server = language_server!().id(); @@ -1135,6 +1229,7 @@ impl Application { ); } } + Call::Invalid { id } => log::error!("LSP invalid method call id={:?}", id), } } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 4c12b0239854..079e652d907b 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 syntax; pub(crate) mod typed; @@ -12,7 +13,10 @@ use helix_stdx::{ }; use helix_vcs::{FileChange, Hunk}; pub use lsp::*; + +pub use engine::ScriptingEngine; pub use syntax::*; + use tui::{ text::{Span, Spans}, widgets::Cell, @@ -261,7 +265,10 @@ impl MappableCommand { cx.editor.set_error(format!("{}", e)); } } else { - cx.editor.set_error(format!("no such command: '{name}'")); + 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), @@ -301,6 +308,15 @@ impl MappableCommand { } } + #[cfg(feature = "steel")] + 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", @@ -652,21 +668,19 @@ impl std::str::FromStr for MappableCommand { if let Some(suffix) = s.strip_prefix(':') { let (name, args, _) = command_line::split(suffix); ensure!(!name.is_empty(), "Expected typable command name"); - typed::TYPABLE_COMMAND_MAP + let typable = 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(), }) - .ok_or_else(|| anyhow!("No TypableCommand named '{}'", s)) + .unwrap_or_else(|| MappableCommand::Typable { + name: name.to_owned(), + args: args.to_owned(), + doc: "Undocumented plugin command".to_string(), + }); + Ok(typable) } else if let Some(suffix) = s.strip_prefix('@') { helix_view::input::parse_macro(suffix).map(|keys| Self::Macro { name: s.to_string(), @@ -3413,15 +3427,27 @@ pub fn command_palette(cx: &mut Context) { [&cx.editor.mode] .reverse_map(); - let commands = MappableCommand::STATIC_COMMAND_LIST.iter().cloned().chain( - typed::TYPABLE_COMMAND_LIST - .iter() - .map(|cmd| MappableCommand::Typable { - name: cmd.name.to_owned(), + let commands = MappableCommand::STATIC_COMMAND_LIST + .iter() + .cloned() + .chain( + typed::TYPABLE_COMMAND_LIST + .iter() + .map(|cmd| MappableCommand::Typable { + name: cmd.name.to_owned(), + args: String::new(), + doc: cmd.doc.to_owned(), + }), + ) + .chain(ScriptingEngine::available_commands().into_iter().map(|x| { + let doc = ScriptingEngine::get_doc_for_identifier(&x).unwrap_or_default(); + + MappableCommand::Typable { + name: x.into_owned(), args: String::new(), - doc: cmd.doc.to_owned(), - }), - ); + doc, + } + })); let columns = [ ui::PickerColumn::new("name", |item, _| match item { diff --git a/helix-term/src/commands/engine.rs b/helix-term/src/commands/engine.rs new file mode 100644 index 000000000000..bee818e765df --- /dev/null +++ b/helix-term/src/commands/engine.rs @@ -0,0 +1,324 @@ +use arc_swap::{ArcSwap, ArcSwapAny}; +use helix_core::syntax; +use helix_lsp::{jsonrpc, LanguageServerId}; +use helix_view::{document::Mode, input::KeyEvent}; + +#[cfg(unix)] +use termina::{EventReader, Terminal}; + +use std::{borrow::Cow, sync::Arc}; + +use crate::{ + application::TerminalBackend, + compositor, + config::Config, + keymap::KeymapResult, + ui::{self, PromptEvent}, +}; + +use super::Context; + +#[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), +} + +const DEFAULT_PLUGIN_PRECEDENCE: &[PluginSystemTypes] = &[ + #[cfg(feature = "steel")] + PluginSystemTypes::Steel(steel::SteelScriptingEngine), + PluginSystemTypes::None(NoEngine), +]; + +static PLUGIN_PRECEDENCE: once_cell::sync::OnceCell> = + once_cell::sync::OnceCell::new(); + +fn plugins() -> impl Iterator { + PLUGIN_PRECEDENCE.get().unwrap().iter() +} + +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),*), + } + }; +} + +#[cfg(any(windows, feature = "integration"))] +#[derive(Clone)] +pub struct TerminalEventReaderHandle; + +#[cfg(any(windows, feature = "integration"))] +impl TerminalEventReaderHandle { + pub fn new(terminal: &TerminalBackend) -> Self { + Self + } +} + +#[allow(unused)] +#[cfg(all(not(windows), not(feature = "integration")))] +#[derive(Clone)] +pub struct TerminalEventReaderHandle { + reader: EventReader, +} + +#[cfg(all(not(windows), not(feature = "integration")))] +impl TerminalEventReaderHandle { + pub fn new(terminal: &TerminalBackend) -> Self { + Self { + reader: terminal.terminal().event_reader(), + } + } +} + +impl ScriptingEngine { + pub fn initialize() { + for kind in DEFAULT_PLUGIN_PRECEDENCE { + manual_dispatch!(kind, initialize()) + } + } + + pub fn reinitialize() { + for kind in DEFAULT_PLUGIN_PRECEDENCE { + manual_dispatch!(kind, reinitialize()) + } + } + + pub fn run_initialization_script( + cx: &mut Context, + configuration: Arc>>, + language_configuration: Arc>, + event_reader: TerminalEventReaderHandle, + ) { + // Set up a flag to disable steel, even on the current build? + if configuration.load().editor.enable_steel { + PLUGIN_PRECEDENCE + .set(vec![ + #[cfg(feature = "steel")] + PluginSystemTypes::Steel(steel::SteelScriptingEngine), + PluginSystemTypes::None(NoEngine), + ]) + .ok(); + } else { + PLUGIN_PRECEDENCE + .set(vec![PluginSystemTypes::None(NoEngine)]) + .ok(); + } + + for kind in plugins() { + manual_dispatch!( + kind, + run_initialization_script( + cx, + configuration.clone(), + language_configuration.clone(), + event_reader.clone() + ) + ) + } + } + + pub fn handle_keymap_event( + editor: &mut ui::EditorView, + mode: Mode, + cxt: &mut Context, + event: KeyEvent, + ) -> Option { + for kind in plugins() { + 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 plugins() { + 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 plugins() { + 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 plugins() { + let doc = manual_dispatch!(kind, get_doc_for_identifier(ident)); + + if doc.is_some() { + return doc; + } + } + + None + } + + pub fn available_commands<'a>() -> Vec> { + plugins() + .flat_map(|kind| manual_dispatch!(kind, available_commands())) + .collect() + } + + pub fn handle_lsp_call( + cx: &mut compositor::Context, + server_id: LanguageServerId, + event_name: String, + call_id: jsonrpc::Id, + params: jsonrpc::Params, + ) -> Option> { + for kind in plugins() { + if let Some(value) = manual_dispatch!( + kind, + handle_lsp_call( + cx, + server_id, + event_name.clone(), + call_id.clone(), + params.clone() + ) + ) { + return Some(value); + } + } + + None + } + + pub fn generate_sources() { + for kind in DEFAULT_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) {} + + /// Any work that needs to be done to unload the existing engine in preparation + /// for a new run. + fn reinitialize(&self) {} + + #[allow(unused)] + 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>, + _event_reader: TerminalEventReaderHandle, + ) { + } + + /// 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 + } + + /// Call into the scripting engine to handle an unhandled LSP notification, sent from the server + /// to the client. + #[inline(always)] + fn handle_lsp_call( + &self, + _cx: &mut compositor::Context, + _server_id: LanguageServerId, + _event_name: String, + _call_id: jsonrpc::Id, + _params: jsonrpc::Params, + ) -> Option> { + None + } + + /// 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/steel/components.rs b/helix-term/src/commands/engine/steel/components.rs new file mode 100644 index 000000000000..a7fd7c85ec5d --- /dev/null +++ b/helix-term/src/commands/engine/steel/components.rs @@ -0,0 +1,2176 @@ +use std::{collections::HashMap, str::FromStr, 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, AsRefSteelVal, Custom, FromSteelVal, IntoSteelVal, SteelString}, + steel_vm::{builtin::BuiltInModule, engine::Engine, register_fn::RegisterFn}, + RootedSteelVal, 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::{ + enter_engine, format_docstring, is_current_generation, load_generation, + 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 self + .channel + .send(String::from_utf8_lossy(buf).into_owned()) + .is_err() + { + Ok(0) + } else { + Ok(buf.len()) + } + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +pub fn helix_component_module(generate_sources: bool) -> BuiltInModule { + let mut module = BuiltInModule::new("helix/components"); + + let mut builtin_components_module = if generate_sources { + "(require-builtin helix/components as helix.components.)".to_owned() + } else { + String::new() + }; + + macro_rules! register { + (value, $name:expr, $function:expr, $doc:expr) => { + module.register_value($name, $function); + { + let doc = format_docstring($doc); + builtin_components_module.push_str(&format!( + r#" +(provide {}) +;;@doc +{} +(define {} helix.components.{}) + "#, + $name, doc, $name, $name + )); + } + }; + + (value, $name:expr, $function:expr) => { + module.register_value($name, $function); + { + builtin_components_module.push_str(&format!( + r#" +(provide {}) +(define {} helix.components.{}) + "#, + $name, $name, $name + )); + } + }; + + ($name:expr, $function:expr, $doc:expr) => { + module.register_fn($name, $function); + { + let doc = format_docstring($doc); + builtin_components_module.push_str(&format!( + r#" +(provide {}) +;;@doc +{} +(define {} helix.components.{}) + "#, + $name, doc, $name, $name + )); + } + }; + + ($name:expr, $function:expr) => { + module.register_fn($name, $function); + { + builtin_components_module.push_str(&format!( + r#" +(provide {}) +(define {} helix.components.{}) + "#, + $name, $name, $name + )); + } + }; + + (ctx, $name:expr, $function:expr, $arity:expr, $doc:expr) => { + module.register_fn($name, $function); + let mut function_expr = Vec::with_capacity($arity); + for arg in 0..$arity { + function_expr.push(format!("arg{}", arg)); + } + + let formatted = function_expr.join(" "); + + { + let doc = format_docstring($doc); + builtin_components_module.push_str(&format!( + r#" +(provide {}) +;;@doc +{} +(define ({} {}) (helix.components.{} *helix.cx* {})) + "#, + $name, doc, $name, &formatted, $name, &formatted + )); + } + }; + } + + register!("async-read-line", AsyncReader::read_line); + register!("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(), + ] + }); + register!( + "theme->bg", + |ctx: &mut Context| { ctx.editor.theme.get("ui.background") }, + "Gets the `Style` associated with the bg for the current theme" + ); + register!( + "theme->fg", + |ctx: &mut Context| { ctx.editor.theme.get("ui.text") }, + "Gets the `style` associated with the fg for the current theme" + ); + register!( + ctx, + "theme-scope", + |ctx: &mut Context, scope: SteelString| { ctx.editor.theme.get(scope.as_str()) }, + 1, + "Get the `Style` associated with the given scope from the current theme" + ); + + register!( + "Position?", + |position: SteelVal| { Position::as_ref(&position).is_ok() }, + r#"Check if the given value is a `Position` + +```scheme +(Position? value) -> bool? +``` + +value : any? + + "# + ); + + register!( + "Style?", + |style: SteelVal| Style::as_ref(&style).is_ok(), + r#"Check if the given valuie is `Style` + +```scheme +(Style? value) -> bool? +``` + +value : any? +"# + ); + + register!( + "Buffer?", + |value: SteelVal| steel::gc::is_reference_type::(&value), + r#" +Checks if the given value is a `Buffer` + +```scheme +(Buffer? value) -> bool? +``` + +value : any? + "# + ); + + register!( + "buffer-area", + |buffer: &mut Buffer| buffer.area, + r#" +Get the `Rect` associated with the given `Buffer` + +```scheme +(buffer-area buffer) +``` + +* buffer : Buffer? + "# + ); + + register!( + "frame-set-string!", + buffer_set_string, + r#" +Set the string at the given `x` and `y` positions for the given `Buffer`, with a provided `Style`. + +```scheme +(frame-set-string! buffer x y string style) +``` + +buffer : Buffer?, +x : int?, +y : int?, +string: string?, +style: Style?, + "# + ); + + // name: String, + // state: SteelVal, + // render: SteelVal, + // h: HashMap, + // 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(), + + register!( + "SteelEventResult?", + |value: SteelVal| { SteelEventResult::as_ref(&value).is_ok() }, + r#" +Check whether the given value is a `SteelEventResult`. + +```scheme +(SteelEventResult? value) -> bool? +``` + +value : any? + + "# + ); + + register!( + "new-component!", + SteelDynamicComponent::new_dyn, + r#" +Construct a new dynamic component. This is used for creating widgets or floating windows +that exist outside of the buffer. This just constructs the component, it does not push the component +on to the component stack. For that, you'll use `push-component!`. + +```scheme +(new-component! name state render function-map) +``` + +name : string? - This is the name of the comoponent itself. +state : any? - Typically this is a struct that holds the state of the component. +render : (-> state? Rect? Buffer?) + This is a function that will get called with each frame. The first argument is the state object provided, + and the second is the `Rect?` to render against, ultimately against the `Buffer?`. + +function-map : (hashof string? function?) + This is a hashmap of strings -> function that contains a few important functions: + + "handle_event" : (-> state? Event?) -> SteelEventResult? + + This is called on every event with an event object. There are multiple options you can use + when returning from this function: + + * event-result/consume + * event-result/consume-without-rerender + * event-result/ignore + * event-result/close + + See the associated docs for those to understand the implications for each. + + "cursor" : (-> state? Rect?) -> Position? + + This tells helix where to put the cursor. + + "required_size": (-> state? (pair? int?)) -> (pair? int?) + + Seldom used: TODO + "# + ); + + register!( + "position", + Position::new, + r#" +Construct a new `Position`. + +```scheme +(position row col) -> Position? +``` + +row : int? +col : int? + "# + ); + register!( + "position-row", + |position: &Position| position.row, + r#" +Get the row associated with the given `Position`. + +```scheme +(position-row pos) -> int? +``` + +pos : `Position?` + "# + ); + register!( + "position-col", + |position: &Position| position.col, + r#" +Get the col associated with the given `Position`. + +```scheme +(position-col pos) -> int? +``` + +pos : `Position?` +"# + ); + + register!( + "set-position-row!", + |position: &mut Position, row: usize| { + position.row = row; + }, + r#"Set the row for the given `Position` + +```scheme +(set-position-row! pos row) +``` + +pos : Position? +row : int? + "# + ); + register!( + "set-position-col!", + |position: &mut Position, col: usize| { + position.col = col; + }, + r#"Set the col for the given `Position` + +```scheme +(set-position-col! pos col) +``` + +pos : Position? +col : int? + "# + ); + + register!( + "Rect?", + |value: SteelVal| { Rect::as_ref(&value).is_ok() }, + r#"Check if the given value is a `Rect` + +```scheme +(Rect? value) -> bool? +``` + +value : any? + + "# + ); + + register!( + "area", + helix_view::graphics::Rect::new, + r#" +Constructs a new `Rect`. + +(area x y width height) + +* x : int? +* y : int? +* width: int? +* height: int? + +# Examples + +```scheme +(area 0 0 100 200) +``` +"# + ); + register!( + "area-x", + |area: &helix_view::graphics::Rect| area.x, + r#"Get the `x` value of the given `Rect` + +```scheme +(area-x area) -> int? +``` + +area : Rect? + "# + ); + register!( + "area-y", + |area: &helix_view::graphics::Rect| area.y, + r#"Get the `y` value of the given `Rect` + +```scheme +(area-y area) -> int? +``` + +area : Rect? + "# + ); + register!( + "area-width", + |area: &helix_view::graphics::Rect| area.width, + r#"Get the `width` value of the given `Rect` + +```scheme +(area-width area) -> int? +``` + +area : Rect? + "# + ); + register!( + "area-height", + |area: &helix_view::graphics::Rect| { area.height }, + r#"Get the `height` value of the given `Rect` + +```scheme +(area-height area) -> int? +``` + +area : Rect? + "# + ); + + register!("overlaid", |component: &mut WrappedDynComponent| { + let inner: Option> = + component.inner.take().map(|x| { + Box::new(overlaid(BoxDynComponent::new(x))) + as Box + }); + + component.inner = inner; + }); + + register!( + "Widget/list?", + |value: SteelVal| { widgets::List::as_ref(&value).is_ok() }, + r#"Check whether the given value is a list widget. + +```scheme +(Widget/list? value) -> bool? +``` + +value : any? + "# + ); + + register!( + "widget/list", + |items: Vec| { + widgets::List::new( + items + .into_iter() + .map(|x| ListItem::new(Text::from(x))) + .collect::>(), + ) + }, + r#"Creates a new `List` widget with the given items. + +```scheme +(widget/list lst) -> Widget? +``` + +* lst : (listof string?) + "# + ); + + register!( + "widget/list/render", + |buf: &mut Buffer, area: Rect, list: widgets::List| list.render(area, buf), + r#" + +Render the given `Widget/list` onto the provided `Rect` within the given `Buffer`. + +```scheme +(widget/list/render buf area lst) +``` + +* buf : `Buffer?` +* area : `Rect?` +* lst : `Widget/list?` + "# + ); + + register!( + "block", + || { + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::White)) + .border_type(BorderType::Rounded) + .style(Style::default().bg(Color::Black)) + }, + r#"Creates a block with the following styling: + +```scheme +(block) +``` + +* borders - all +* border-style - default style + white fg +* border-type - rounded +* style - default + black bg + "# + ); + + register!( + "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) + }, + r#" +Create a `Block` with the provided styling, borders, and border type. + + +```scheme +(make-block style border-style borders border_type) +``` + +* style : Style? +* border-style : Style? +* borders : string? +* border-type: String? + +Valid border-types include: +* "plain" +* "rounded" +* "double" +* "thick" + +Valid borders include: +* "top" +* "left" +* "right" +* "bottom" +* "all" + "# + ); + + register!( + "block/render", + |buf: &mut Buffer, area: Rect, block: Block| block.render(area, buf), + r#" +Render the given `Block` over the given `Rect` onto the provided `Buffer`. + +```scheme +(block/render buf area block) +``` + +buf : Buffer? +area: Rect? +block: Block? + + "# + ); + + register!( + "buffer/clear", + Buffer::clear, + r#"Clear a `Rect` in the `Buffer` + +```scheme +(buffer/clear frame area) +``` +frame : Buffer? +area : Rect? + "# + ); + + register!( + "buffer/clear-with", + Buffer::clear_with, + r#"Clear a `Rect` in the `Buffer` with a default `Style` + +```scheme +(buffer/clear-with frame area style) +``` +frame : Buffer? +area : Rect? +style : Style? + "# + ); + + // Mutate a color in place, to save some headache. + register!( + "set-color-rgb!", + |color: &mut Color, r: u8, g: u8, b: u8| { + *color = Color::Rgb(r, g, b); + }, + r#" +Mutate the r/g/b of a color in place, to avoid allocation. + +```scheme +(set-color-rgb! color r g b) +``` + +color : Color? +r : int? +g : int? +b : int? +"# + ); + + register!( + "set-color-indexed!", + |color: &mut Color, index: u8| { + *color = Color::Indexed(index); + }, + r#" +Mutate this color to be an indexed color. + +```scheme +(set-color-indexed! color index) +``` + +color : Color? +index: int? + +"# + ); + + register!( + "Color?", + |color: SteelVal| { Color::as_ref(&color).is_ok() }, + r#"Check if the given value is a `Color`. + +```scheme +(Color? value) -> bool? +``` + +value : any? + + "# + ); + + register!( + value, + "Color/Reset", + Color::Reset.into_steelval().unwrap(), + r#" +Singleton for the reset color. + "# + ); + register!( + value, + "Color/Black", + Color::Black.into_steelval().unwrap(), + r#" +Singleton for the color black. + "# + ); + register!( + value, + "Color/Red", + Color::Red.into_steelval().unwrap(), + r#" +Singleton for the color red. + "# + ); + register!( + value, + "Color/White", + Color::White.into_steelval().unwrap(), + r#" +Singleton for the color white. + "# + ); + register!( + value, + "Color/Green", + Color::Green.into_steelval().unwrap(), + r#" +Singleton for the color green. + "# + ); + register!( + value, + "Color/Yellow", + Color::Yellow.into_steelval().unwrap(), + r#" +Singleton for the color yellow. + "# + ); + register!( + value, + "Color/Blue", + Color::Blue.into_steelval().unwrap(), + r#" +Singleton for the color blue. + "# + ); + register!( + value, + "Color/Magenta", + Color::Magenta.into_steelval().unwrap(), + r#" +Singleton for the color magenta. + "# + ); + register!( + value, + "Color/Cyan", + Color::Cyan.into_steelval().unwrap(), + r#" +Singleton for the color cyan. + "# + ); + register!( + value, + "Color/Gray", + Color::Gray.into_steelval().unwrap(), + r#" +Singleton for the color gray. + "# + ); + register!( + value, + "Color/LightRed", + Color::LightRed.into_steelval().unwrap(), + r#" +Singleton for the color light read. + "# + ); + register!( + value, + "Color/LightGreen", + Color::LightGreen.into_steelval().unwrap(), + r#" +Singleton for the color light green. + "# + ); + register!( + value, + "Color/LightYellow", + Color::LightYellow.into_steelval().unwrap(), + r#" +Singleton for the color light yellow. + "# + ); + register!( + value, + "Color/LightBlue", + Color::LightBlue.into_steelval().unwrap(), + r#" +Singleton for the color light blue. + "# + ); + register!( + value, + "Color/LightMagenta", + Color::LightMagenta.into_steelval().unwrap(), + r#" +Singleton for the color light magenta. + "# + ); + register!( + value, + "Color/LightCyan", + Color::LightCyan.into_steelval().unwrap(), + r#" +Singleton for the color light cyan. + "# + ); + register!( + value, + "Color/LightGray", + Color::LightGray.into_steelval().unwrap(), + r#" +Singleton for the color light gray. + "# + ); + + register!( + "Color/rgb", + Color::Rgb, + r#" +Construct a new color via rgb. + +```scheme +(Color/rgb r g b) -> Color? +``` + +r : int? +g : int? +b : int? + "# + ); + register!( + "Color-red", + Color::red, + r#" +Get the red component of the `Color?`. + +```scheme +(Color-red color) -> int? +``` + +color * Color? + "# + ); + register!( + "Color-green", + Color::green, + r#" +Get the green component of the `Color?`. + +```scheme +(Color-green color) -> int? +``` + +color * Color? +"# + ); + register!( + "Color-blue", + Color::blue, + r#" +Get the blue component of the `Color?`. + +```scheme +(Color-blue color) -> int? +``` + +color * Color? +"# + ); + register!( + "Color/Indexed", + Color::Indexed, + r#" + +Construct a new indexed color. + +```scheme +(Color/Indexed index) -> Color? +``` + +* index : int? + "# + ); + + register!( + "set-style-fg!", + |style: &mut Style, color: Color| { + style.fg = Some(color); + }, + r#" + +Mutates the given `Style` to have the fg with the provided color. + +```scheme +(set-style-fg! style color) +``` + +style : `Style?` +color : `Color?` + "# + ); + + register!( + "style-fg", + Style::fg, + r#" + +Constructs a new `Style` with the provided `Color` for the fg. + +```scheme +(style-fg style color) -> Style +``` + +style : Style? +color: Color? + "# + ); + register!( + "style-bg", + Style::bg, + r#" + +Constructs a new `Style` with the provided `Color` for the bg. + +```scheme +(style-bg style color) -> Style +``` + +style : Style? +color: Color? + "# + ); + register!( + "style-with-italics", + |style: &Style| { + let patch = Style::default().add_modifier(Modifier::ITALIC); + style.patch(patch) + }, + r#" + +Constructs a new `Style` with italcs. + +```scheme +(style-with-italics style) -> Style +``` + +style : Style? + "# + ); + register!( + "style-with-bold", + |style: Style| { + let patch = Style::default().add_modifier(Modifier::BOLD); + style.patch(patch) + }, + r#" + +Constructs a new `Style` with bold styling. + +```scheme +(style-with-bold style) -> Style +``` + +style : Style? + "# + ); + register!( + "style-with-dim", + |style: &Style| { + let patch = Style::default().add_modifier(Modifier::DIM); + style.patch(patch) + }, + r#" + +Constructs a new `Style` with dim styling. + +```scheme +(style-with-dim style) -> Style +``` + +style : Style? + "# + ); + register!( + "style-with-slow-blink", + |style: Style| { + let patch = Style::default().add_modifier(Modifier::SLOW_BLINK); + style.patch(patch) + }, + r#" + +Constructs a new `Style` with slow blink. + +```scheme +(style-with-slow-blink style) -> Style +``` + +style : Style? + "# + ); + register!( + "style-with-rapid-blink", + |style: Style| { + let patch = Style::default().add_modifier(Modifier::RAPID_BLINK); + style.patch(patch) + }, + r#" + +Constructs a new `Style` with rapid blink. + +```scheme +(style-with-rapid-blink style) -> Style +``` + +style : Style? + "# + ); + register!( + "style-with-reversed", + |style: Style| { + let patch = Style::default().add_modifier(Modifier::REVERSED); + style.patch(patch) + }, + r#" + +Constructs a new `Style` with revered styling. + +```scheme +(style-with-reversed style) -> Style +``` + +style : Style? + "# + ); + register!( + "style-with-hidden", + |style: Style| { + let patch = Style::default().add_modifier(Modifier::HIDDEN); + style.patch(patch) + }, + r#" +Constructs a new `Style` with hidden styling. + +```scheme +(style-with-hidden style) -> Style +``` + +style : Style? + "# + ); + register!( + "style-with-crossed-out", + |style: Style| { + let patch = Style::default().add_modifier(Modifier::CROSSED_OUT); + style.patch(patch) + }, + r#" + +Constructs a new `Style` with crossed out styling. + +```scheme +(style-with-crossed-out style) -> Style +``` + +style : Style? + "# + ); + register!( + "style->fg", + |style: &Style| style.fg, + r#" + +Return the color on the style, or #false if not present. + +```scheme +(style->fg style) -> (or Color? #false) +``` + +style : Style? + + "# + ); + register!( + "style->bg", + |style: &Style| style.bg, + r#" + +Return the color on the style, or #false if not present. + +```scheme +(style->bg style) -> (or Color? #false) +``` + +style : Style? + + "# + ); + register!( + "set-style-bg!", + |style: &mut Style, color: Color| { + style.bg = Some(color); + }, + r#" + +Mutate the background style on the given style to a given color. + +```scheme +(set-style-bg! style color) +``` + +style : Style? +color : Color? + + "# + ); + + register!( + "style-underline-color", + Style::underline_color, + r#" + +Return a new style with the provided underline color. + +```scheme +(style-underline-color style color) -> Style? + +``` +style : Style? +color : Color? + + "# + ); + register!( + "style-underline-style", + Style::underline_style, + r#" +Return a new style with the provided underline style. + +```scheme +(style-underline-style style underline-style) -> Style? + +``` + +style : Style? +underline-style : UnderlineStyle? + +"# + ); + + register!( + "UnderlineStyle?", + |value: SteelVal| { UnderlineStyle::as_ref(&value).is_ok() }, + r#" +Check if the provided value is an `UnderlineStyle`. + +```scheme +(UnderlineStyle? value) -> bool? + +``` +value : any?"# + ); + + register!( + value, + "Underline/Reset", + UnderlineStyle::Reset.into_steelval().unwrap(), + r#" +Singleton for resetting the underling style. + "# + ); + register!( + value, + "Underline/Line", + UnderlineStyle::Line.into_steelval().unwrap(), + r#" +Singleton for the line underline style. + "# + ); + register!( + value, + "Underline/Curl", + UnderlineStyle::Curl.into_steelval().unwrap(), + r#" +Singleton for the curl underline style. + "# + ); + register!( + value, + "Underline/Dotted", + UnderlineStyle::Dotted.into_steelval().unwrap(), + r#" +Singleton for the dotted underline style. + "# + ); + register!( + value, + "Underline/Dashed", + UnderlineStyle::Dashed.into_steelval().unwrap(), + r#" +Singleton for the dashed underline style. + "# + ); + register!( + value, + "Underline/DoubleLine", + UnderlineStyle::DoubleLine.into_steelval().unwrap(), + r#" +Singleton for the double line underline style. + "# + ); + register!( + value, + "event-result/consume", + SteelEventResult::Consumed.into_steelval().unwrap(), + r#" +Singleton for consuming an event. If this is returned from an event handler, the event +will not continue to be propagated down the component stack. This also will trigger a +re-render. + "# + ); + register!( + value, + "event-result/consume-without-rerender", + SteelEventResult::ConsumedWithoutRerender + .into_steelval() + .unwrap(), + r#" +Singleton for consuming an event. If this is returned from an event handler, the event +will not continue to be propagated down the component stack. This will _not_ trigger +a re-render. + "# + ); + register!( + value, + "event-result/ignore", + SteelEventResult::Ignored.into_steelval().unwrap(), + r#" +Singleton for ignoring an event. If this is returned from an event handler, the event +will not continue to be propagated down the component stack. This will _not_ trigger +a re-render. + "# + ); + + register!( + value, + "event-result/ignore-and-close", + SteelEventResult::IgnoreAndClose.into_steelval().unwrap(), + r#" +Singleton for ignoring an event. If this is returned from an event handler, the event +will continue to be propagated down the component stack, and the component will be +popped off of the stack and removed. + "# + ); + + register!( + value, + "event-result/close", + SteelEventResult::Close.into_steelval().unwrap(), + r#" +Singleton for consuming an event. If this is returned from an event handler, the event +will not continue to be propagated down the component stack, and the component will +be popped off of the stack and removed. + "# + ); + + register!( + "style", + Style::default, + r#" +Constructs a new default style. + +```scheme +(style) -> Style? +``` + "# + ); + + register!( + "Event?", + |value: SteelVal| { Event::as_ref(&value).is_ok() }, + r#"Check if this value is an `Event` + +```scheme +(Event? value) -> bool? +``` +value : any? + "# + ); + + // TODO: Register this differently so it doesn't clone the pasted text unnecessarily + register!( + "paste-event?", + |event: Event| { matches!(event, Event::Paste(_)) }, + r#"Checks if the given event is a paste event. + +```scheme +(paste-event? event) -> bool? +``` + +* event : Event? + + "# + ); + + register!( + "paste-event-string", + |event: Event| { + if let Event::Paste(p) = event { + Some(p) + } else { + None + } + }, + r#"Get the string from the paste event, if it is a paste event. + +```scheme +(paste-event-string event) -> (or string? #false) +``` + +* event : Event? + + "# + ); + + register!( + "key-event?", + |event: Event| { matches!(event, Event::Key(_)) }, + r#"Checks if the given event is a key event. + +```scheme +(key-event? event) -> bool? +``` + +* event : Event? + "# + ); + + register!( + "string->key-event", + |value: SteelString| { KeyEvent::from_str(value.as_str()) }, + "Get a key event from a string" + ); + + register!( + "event->key-event", + |event: Event| { + if let Event::Key(k) = event { + Some(k) + } else { + None + } + }, + "Return the key event from an event, if it is one" + ); + + register!( + "key-event-char", + |event: Event| { + if let Event::Key(event) = event { + event.char() + } else { + None + } + }, + r#"Get the character off of the event, if there is one. + +```scheme +(key-event-char event) -> (or char? #false) +``` +event : Event? + "# + ); + + register!( + "on-key-event-char", + |event: KeyEvent| { event.char() }, + r#"Get the character off of the key event, if there is one. + +```scheme +(on-key-event-char event) -> (or char? #false) +``` +event : KeyEvent? + "# + ); + + register!( + "key-event-modifier", + |event: Event| { + if let Event::Key(KeyEvent { modifiers, .. }) = event { + Some(modifiers.bits()) + } else { + None + } + }, + r#" +Get the key event modifier off of the event, if there is one. + +```scheme +(key-event-modifier event) -> (or int? #false) +``` +event : Event? + "# + ); + + register!( + value, + "key-modifier-ctrl", + SteelVal::IntV(KeyModifiers::CONTROL.bits() as isize), + r#" +The key modifier bits associated with the ctrl key modifier. + "# + ); + register!( + value, + "key-modifier-shift", + SteelVal::IntV(KeyModifiers::SHIFT.bits() as isize), + r#" +The key modifier bits associated with the shift key modifier. + "# + ); + register!( + value, + "key-modifier-alt", + SteelVal::IntV(KeyModifiers::ALT.bits() as isize), + r#" +The key modifier bits associated with the alt key modifier. + "# + ); + register!( + value, + "key-modifier-super", + SteelVal::IntV(KeyModifiers::SUPER.bits() as isize), + r#" +The key modifier bits associated with the super key modifier. + "# + ); + + register!( + "key-event-F?", + |event: Event, number: u8| match event { + Event::Key(KeyEvent { + code: KeyCode::F(x), + .. + }) => number == x, + _ => false, + }, + r#"Check if this key event is associated with an `F` key, e.g. F1, F2, etc. + +```scheme +(key-event-F? event number) -> bool? +``` +event : Event? +number : int? + "# + ); + + register!( + "mouse-event?", + |event: Event| { matches!(event, Event::Mouse(_)) }, + r#" +Check if this event is a mouse event. + +```scheme +(mouse-event event) -> bool? +``` +event : Event? +"# + ); + + register!( + "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() + } + }, + r#"Convert the mouse event kind into an integer representing the state. + +```scheme +(event-mouse-kind event) -> (or int? #false) +``` + +event : Event? + +This is the current mapping today: + +```rust +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, +} +``` + +Any unhandled event that does not match this will return `#false`. +"# + ); + + register!( + "event-mouse-row", + |event: Event| { + if let Event::Mouse(MouseEvent { row, .. }) = event { + row.into_steelval() + } else { + false.into_steelval() + } + }, + r#" + +Get the row from the mouse event, of #false if it isn't a mouse event. + +```scheme +(event-mouse-row event) -> (or int? #false) +``` + +event : Event? + + "# + ); + register!( + "event-mouse-col", + |event: Event| { + if let Event::Mouse(MouseEvent { column, .. }) = event { + column.into_steelval() + } else { + false.into_steelval() + } + }, + r#" + +Get the col from the mouse event, of #false if it isn't a mouse event. + +```scheme +(event-mouse-row event) -> (or int? #false) +``` + +event : Event? + "# + ); + // Is this mouse event within the area provided + register!( + "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 + } + }, + r#"Check whether the given mouse event occurred within a given `Rect`. + +```scheme +(mouse-event-within-area? event area) -> bool? +``` + +event : Event? +area : Rect? + "# + ); + + macro_rules! register_key_events { + ($ ( $name:expr => $key:tt ) , *, ) => { + $( + register!(concat!("key-event-", $name, "?"), |event: Event| { + matches!( + event, + Event::Key( + KeyEvent { + code: KeyCode::$key, + .. + } + )) + }, + &format!(r#" +Check whether the given event is the key: {} + +```scheme +(key-event-{}? event) +``` +event: Event?"#, $name, $name)); + )* + }; + } + + // 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, + "end" => End, + "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, + ); + + if generate_sources { + if let Some(mut target_directory) = + crate::commands::engine::steel::alternative_runtime_search_path() + { + if !target_directory.exists() { + std::fs::create_dir_all(&target_directory).unwrap(); + } + target_directory.push("components.scm"); + std::fs::write(target_directory, &builtin_components_module).unwrap(); + } + } + + module +} + +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, + + // Just root all of the inputs so that we don't have any issues with + // things dropping + _roots: Vec, + + generation: usize, +} + +impl SteelDynamicComponent { + pub fn new( + name: String, + state: SteelVal, + render: SteelVal, + h: HashMap, + ) -> Self { + let mut roots = vec![state.clone().as_rooted(), render.clone().as_rooted()]; + + for value in h.values() { + roots.push(value.clone().as_rooted()); + } + + // Keep root tokens around? Otherwise we're not going to be + // able to reach these values from the runtime. + 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, + _roots: roots, + generation: load_generation(), + } + } + + 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)), + } + } +} + +impl Custom for SteelDynamicComponent {} + +impl Custom for Box {} + +#[derive(Clone)] +enum SteelEventResult { + Consumed, + Ignored, + IgnoreAndClose, + 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, + ) { + if !is_current_generation(self.generation) { + return; + } + + // Skip rendering if the function is actually false + if let SteelVal::BoolV(false) = self.render { + return; + } + + 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], + ) + }; + + let res = 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) + }) + { + let name = self.name.clone(); + super::present_error_inside_engine_context_with_callback( + &mut ctx, + guard, + e, + move |compositor| { + compositor.remove_by_dynamic_name(&name); + }, + ); + } + }); + + super::patch_callbacks(&mut ctx); + + res + } + + // 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 { + // Ignore this event off the stack + if !is_current_generation(self.generation) { + return compositor::EventResult::Ignored(Some(Box::new( + |compositor: &mut compositor::Compositor, _| { + compositor.pop(); + }, + ))); + } + + 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 res = 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) => compositor::EventResult::Consumed(Some( + Box::new(|compositor: &mut compositor::Compositor, _| { + // remove the layer + compositor.pop(); + }), + )), + Ok(SteelEventResult::Consumed) => compositor::EventResult::Consumed(None), + Ok(SteelEventResult::ConsumedWithoutRerender) => { + compositor::EventResult::ConsumedWithoutRerender + } + Ok(SteelEventResult::Ignored) => compositor::EventResult::Ignored(None), + Ok(SteelEventResult::IgnoreAndClose) => compositor::EventResult::Ignored( + Some(Box::new(|compositor: &mut compositor::Compositor, _| { + // remove the layer + compositor.pop(); + })), + ), + _ => 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) + } + }; + + super::patch_callbacks(&mut ctx); + + res + } 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 !is_current_generation(self.generation) { + return (None, helix_view::graphics::CursorKind::Hidden); + } + + 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 cursor_call_result = enter_engine(thunk); + + match cursor_call_result { + Ok(c) => match c { + // Specify the style of the list + SteelVal::ListV(generic_list) => { + if generic_list.len() != 2 { + log::info!("Error: Unable to destructure list of length: {} while setting the cursor position", generic_list.len()); + (None, CursorKind::Block) + } else { + let maybe_position = Option::::from_steelval( + generic_list.get(0).unwrap(), + ); + + let cursor_kind = match generic_list.get(1) { + Some(SteelVal::SymbolV(s) | SteelVal::StringV(s)) => { + match s.as_str() { + "block" => CursorKind::Block, + "hidden" => CursorKind::Hidden, + "bar" => CursorKind::Bar, + "underline" => CursorKind::Underline, + _ => CursorKind::Block, + } + } + + _ => CursorKind::Block, + }; + + match maybe_position { + Ok(v) => (v, cursor_kind), + Err(e) => { + log::info!("Error: {:?}", e); + (None, cursor_kind) + } + } + } + } + other => { + let result = Option::::from_steelval(&other); + 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) + } + } + } + }, + Err(e) => { + log::info!("Error: {:?}", e); + (None, CursorKind::Block) + } + } + + // let result = + // Option::::from_steelval(&enter_engine(|x| thunk(x).unwrap())); + } else { + (None, helix_view::graphics::CursorKind::Hidden) + } + } + + fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { + if !is_current_generation(self.generation) { + return None; + } + + 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. + 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() + .flatten() + } 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/keymaps.scm b/helix-term/src/commands/engine/steel/keymaps.scm new file mode 100644 index 000000000000..6157c76f882b --- /dev/null +++ b/helix-term/src/commands/engine/steel/keymaps.scm @@ -0,0 +1,248 @@ +(require-builtin helix/core/keymaps as helix.keymaps.) + +(require "helix/configuration.scm") + +(provide *reverse-buffer-map-insert* + merge-keybindings + set-global-buffer-or-extension-keymap + add-global-keybinding + deep-copy-global-keybindings + keymap + query-keymap + query-global-keymap) + +(define (get-doc name) + ;; Do our best - if the identifier doesn't exist (for example, if we're checking) + ;; something like 'no_op, we should just continue + (with-handler (lambda (_) #f) + (eval `(#%function-ptr-table-get #%function-ptr-table ,(string->symbol name))))) + +(define (get-typed-command-doc name) + (get-doc (trim-start-matches name ":"))) + +(define (walk-leaves keybindings) + (if (hash? keybindings) (map walk-leaves (hash-values->list keybindings)) keybindings)) + +(define (keybindings->leaves keybindings) + (flatten (walk-leaves keybindings))) + +(define (keybindings->docs keybindings) + (define leaves + (map (lambda (key) (if (symbol? key) (symbol->string key) key)) + (keybindings->leaves keybindings))) + + ;; Filter out anything without values - so we only want strings + (define doc-map + (map (lambda (command) (cons (trim-start-matches command ":") (get-typed-command-doc command))) + leaves)) + + ;; Filter out anything without values - so we only want strings on the + ;; right hand side + (transduce doc-map (filtering (lambda (p) (string? (cdr p)))) (into-hashmap))) + +;;@doc +;; Insert a value into the reverse buffer map +(define (*reverse-buffer-map-insert* key value) + (helix.keymaps.#%add-reverse-mapping key value)) + +;; Marshall values in and out of keybindings, referencing the associated values +;; within steel +(define (merge-keybindings keymap steel-key-map) + (helix.keymaps.helix-merge-keybindings + keymap + (~> steel-key-map (value->jsexpr-string) (helix.keymaps.helix-string->keymap))) + + (helix.keymaps.keymap-update-documentation! keymap (keybindings->docs steel-key-map)) + + keymap) + +;;@doc +;; Check that the types on this map check out, otherwise we don't need to consistently do these checks +(define (set-global-buffer-or-extension-keymap map) + (transduce map + (into-for-each (lambda (p) + (helix.keymaps.#%add-extension-or-labeled-keymap (list-ref p 0) + (list-ref p 1)))))) + +(define query-keymap helix.keymaps.query-keymap) + +;;@doc +;; Query the global keybindings. +;; +;;```scheme +;; (query-global-keymap "normal" '("space" "f")) ;; => "file_picker" +;; ``` +(define (query-global-keymap mode seq) + (helix.keymaps.query-keymap (get-keybindings) mode seq)) + +;;@doc +;; Add keybinding to the global default +(define (add-global-keybinding map) + + ;; Copy the global ones + (define global-bindings (get-keybindings)) + + (merge-keybindings global-bindings map) + + (keybindings global-bindings)) + +;;@doc +;; Deep copy the global keymap +(define (deep-copy-global-keybindings) + + ;; Copy the global keybindings directly + ;; off of the configuration object + (get-keybindings)) + +(define (merge-values left right) + (cond + [(and (list? left) (list? right)) (append left right)] + [(and (hash? left) (hash? right)) + (define merged + (transduce left + (mapping (lambda (p) + (define key (list-ref p 0)) + (define value (list-ref p 1)) + (if (hash-contains? right key) + (cons key (merge-values value (hash-get right key))) + (cons key value)))) + (into-hashmap))) + + (define rhs-keys-not-present + (transduce right + (filtering (lambda (p) (not (hash-contains? merged (car p))))) + (into-hashmap))) + (hash-union merged rhs-keys-not-present)] + [else right])) + +(define (hash-insert-or-merge hm key value) + (if (hash-contains? hm key) + (begin + (let ([existing-value (hash-get hm key)]) + (hash-insert hm key (merge-values existing-value value)))) + (hash-insert hm key value))) + +(define-syntax #%keybindings + (syntax-rules () + + [(_ conf) conf] + + [(_ conf (key (value ...))) + (let ([rhs (#%keybindings (hash) (value ...))]) + (hash-insert-or-merge conf + (if (string? (quote key)) (quote key) (symbol->string (quote key))) + rhs))] + + [(_ conf (key (value ...) rest ...) other ...) + + (let ([left (#%keybindings (hash) other ...)] + [right (#%keybindings (hash) (value ...) rest ...)]) + + (hash-union conf + (hash-insert-or-merge + left + (if (string? (quote key)) (quote key) (symbol->string (quote key))) + right)))] + + [(_ conf (key (value ...) rest ...)) + + (let ([right (#%keybindings (hash) (value ...) rest ...)]) + (hash-insert-or-merge conf + (if (string? (quote key)) (quote key) (symbol->string (quote key))) + right))] + + [(_ conf (key value)) + + (hash-insert-or-merge + conf + (if (string? (quote key)) (quote key) (symbol->string (quote key))) + (if (string? value) value (~>> (quote value) symbol->string (string-append ":"))))] + + [(_ conf (key (value ...)) rest ...) + + (let ([first (#%keybindings (hash) (value ...))] + [inner (hash-insert-or-merge + conf + (if (string? (quote key)) (quote key) (symbol->string (quote key))) + first)]) + + (#%keybindings inner rest ...))] + + [(_ conf (key value) rest ...) + + (let ([inner + (hash-insert-or-merge + conf + (if (string? (quote key)) (quote key) (symbol->string (quote key))) + (if (string? value) value (~>> (quote value) symbol->string (string-append ":"))))]) + + (#%keybindings inner rest ...))])) + +(define (empty-map) + (define the-empty-keymap (hash 'normal (hash) 'insert (hash) 'select (hash))) + (define (hash->keymap steel-key-map) + (~> steel-key-map (value->jsexpr-string) (helix.keymaps.helix-string->keymap))) + + (hash->keymap the-empty-keymap)) + +;;@doc +;; Syntax: +;; +;; Registers a keymap. This is a macro that encapsulates defining either a global +;; keymap, or a buffer / extension specific keybinding. +;; +;; For defining a global keybinding, you provide the `(global)` argument like so: +;; ```scheme +;; (keymap (global) +;; (normal (C-r (f ":recentf-open-files") (s ":show-splash")) +;; (space (l ":load-buffer") (o ":eval-sexpr")))) +;; ``` +;; +;; For defining buffer or extension specfic keybindings, you can provide the following: +;; ```scheme +;; (keymap (extension "scm") (insert (ret ":scheme-indent") (C-l ":insert-lambda"))) +;; (keymap (buffer FILE-TREE) (with-map FILE-TREE-KEYBINDINGS)) +;; ;; with-map says to explicitly use the provided hash +;; ;; for the keybindings. +;; ``` +(define-syntax keymap + (syntax-rules (global insert normal select with-map inherit-from extension buffer) + + [(_ (global) args ...) (add-global-keybinding (keymap args ...))] + + [(_ (extension name (inherit-from kmap)) (with-map bindings)) + (helix.keymaps.#%add-extension-or-labeled-keymap name (merge-keybindings kmap bindings))] + + [(_ (extension name (inherit-from map)) args ...) + (helix.keymaps.#%add-extension-or-labeled-keymap name + (merge-keybindings kmap (keymap args ...)))] + + ;; Add option to not inherit explicitly + [(_ (extension name) (with-map bindings)) + (helix.keymaps.#%add-extension-or-labeled-keymap name (merge-keybindings (empty-map) bindings))] + + [(_ (extension name) args ...) + (helix.keymaps.#%add-extension-or-labeled-keymap name + (merge-keybindings (empty-map) + (keymap args ...)))] + + ;; Expand to the same thing since the underlying + ;; infrastructure is the same + [(_ (buffer name (inherit-from kmap)) (with-map bindings)) + (keymap (extension name (inherit-from kmap)) (with-map bindings))] + [(_ (buffer name (inherit-from kmap)) args ...) + (keymap (extension name (inherit-from kmap)) args ...)] + + [(_ (buffer name) (with-map bindings)) (keymap (extension name) (with-map bindings))] + [(_ (buffer name) args ...) (keymap (extension name) args ...)] + + [(_) (hash)] + + [(_ (insert args ...) rest ...) + (hash-union (#%keybindings (hash) ("insert" args ...)) (keymap rest ...))] + + [(_ (normal args ...) rest ...) + (hash-union (#%keybindings (hash) ("normal" args ...)) (keymap rest ...))] + + [(_ (select args ...) rest ...) + (hash-union (#%keybindings (hash) ("select" args ...)) (keymap rest ...))])) diff --git a/helix-term/src/commands/engine/steel/mod.rs b/helix-term/src/commands/engine/steel/mod.rs new file mode 100644 index 000000000000..86241cccd944 --- /dev/null +++ b/helix-term/src/commands/engine/steel/mod.rs @@ -0,0 +1,6998 @@ +mod components; + +use arc_swap::{ArcSwap, ArcSwapAny}; +use helix_core::{ + command_line::Args, + diagnostic::Severity, + extensions::steel_implementations::{rope_module, SteelRopeSlice}, + find_workspace, graphemes, + syntax::{ + self, + config::{ + default_timeout, AutoPairConfig, LanguageConfiguration, LanguageServerConfiguration, + SoftWrap, + }, + }, + text_annotations::InlineAnnotation, + Range, Selection, Tendril, Transaction, +}; +use helix_event::register_hook; +use helix_lsp::jsonrpc; +use helix_view::{ + annotations::diagnostics::DiagnosticFilter, + document::{DocumentInlayHints, DocumentInlayHintsId, Mode}, + editor::{ + Action, AutoSave, BufferLine, ClippingConfiguration, ConfigEvent, CursorShapeConfig, + FilePickerConfig, GutterConfig, IndentGuidesConfig, LineEndingConfig, LineNumber, + SearchConfig, SmartTabConfig, StatusLineElement, TerminalConfig, WhitespaceConfig, + WhitespaceRender, WhitespaceRenderValue, + }, + events::{DocumentDidOpen, DocumentFocusLost, DocumentSaved, SelectionDidChange}, + extension::document_id_to_usize, + graphics::CursorKind, + input::KeyEvent, + theme::Color, + DocumentId, Editor, Theme, ViewId, +}; +use once_cell::sync::{Lazy, OnceCell}; +use serde_json::Value; +use steel::{ + compiler::modules::steel_home, + gc::{unsafe_erased_pointers::CustomReference, ShareableMut}, + rerrs::ErrorKind, + rvals::{as_underlying_type, AsRefMutSteelVal, FromSteelVal, IntoSteelVal, SteelString}, + steel_vm::{ + engine::Engine, mutex_lock, mutex_unlock, register_fn::RegisterFn, ThreadStateController, + }, + steelerr, RootedSteelVal, SteelErr, SteelVal, +}; +use termina::EventReader; + +use std::{ + borrow::Cow, + collections::HashMap, + error::Error, + io::Write, + num::NonZeroU8, + path::PathBuf, + sync::{ + atomic::{AtomicBool, AtomicUsize, Ordering}, + Mutex, MutexGuard, RwLock, RwLockReadGuard, Weak, + }, + time::{Duration, SystemTime}, +}; +use std::{str::FromStr as _, sync::Arc}; + +use steel::{rvals::Custom, steel_vm::builtin::BuiltInModule}; + +use crate::{ + commands::{insert, TYPABLE_COMMAND_LIST}, + compositor::{self, Component, Compositor}, + config::Config, + events::{OnModeSwitch, PostCommand, PostInsertChar}, + job::{self, Callback}, + keymap::{self, merge_keys, KeyTrie, KeymapResult, MappableCommand}, + ui::{self, picker::PathOrId, PickerColumn, Popup, Prompt, PromptEvent}, +}; + +use components::SteelDynamicComponent; + +use components::helix_component_module; + +use super::{Context, TerminalEventReaderHandle}; +use insert::insert_char; + +static INTERRUPT_HANDLER: Lazy>>> = + Lazy::new(|| Mutex::new(None)); +static SAFEPOINT_HANDLER: Lazy>>> = + Lazy::new(|| Mutex::new(None)); + +static GLOBAL_OFFSET: AtomicUsize = AtomicUsize::new(0); + +static EVENT_READER: OnceCell = OnceCell::new(); + +fn install_event_reader(event_reader: TerminalEventReaderHandle) { + #[cfg(feature = "integration")] + {} + + #[cfg(all(not(windows), not(feature = "integration")))] + { + EVENT_READER.set(event_reader.reader).ok(); + } +} + +fn reload_engine() { + enter_engine(|engine| { + // Install a new generation. Anything using the old engine at this point + // should (hopefully) gracefully go out of scope. + increment_generation(); + + reset_buffer_extension_keymap(); + reset_lsp_call_registry(); + + *engine = setup(); + }) +} + +fn setup() -> Engine { + let engine = steel::steel_vm::engine::Engine::new(); + + let controller = engine.get_thread_state_controller(); + let running = Arc::new(AtomicBool::new(false)); + + let current_generation = load_generation(); + + fn is_event_available() -> std::io::Result { + #[cfg(windows)] + { + crossterm::event::poll(Duration::from_millis(1)) + } + + #[cfg(unix)] + { + EVENT_READER + .get() + .unwrap() + .poll(Some(Duration::from_millis(0)), |_| true) + } + } + + 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; + + while is_current_generation(current_generation) { + std::thread::park(); + + while running.load(std::sync::atomic::Ordering::Relaxed) { + #[cfg(unix)] + if is_event_available().unwrap_or(false) { + let event = EVENT_READER.get().unwrap().read(|_| true); + + if let Ok(termina::Event::Key(termina::event::KeyEvent { + code: termina::event::KeyCode::Char('c'), + modifiers: termina::event::Modifiers::CONTROL, + .. + })) = event + { + controller.interrupt(); + break; + } + } + + #[cfg(windows)] + if is_event_available().unwrap_or(false) { + use crossterm::event::{Event, KeyCode, KeyModifiers}; + let event = crossterm::event::read(); + + if let Ok(Event::Key(crossterm::event::KeyEvent { + code: KeyCode::Char('c'), + modifiers: KeyModifiers::CONTROL, + .. + })) = event + { + controller.interrupt(); + break; + } + } + } + } + }); + + let running_command = Arc::new(AtomicBool::new(true)); + let running_command_clone = running_command.clone(); + + // Put the engine in a place where we can make substantive progress. + let safepoint_wakeup = std::thread::spawn(move || { + let running_command = running_command_clone; + + while is_current_generation(current_generation) { + // If this is running, don't acquire the lock. Park until we're back. + if running_command.load(std::sync::atomic::Ordering::Relaxed) { + std::thread::park(); + continue; + } + + GLOBAL_ENGINE.lock().unwrap().enter_safepoint(|| { + // Set the thread to running, and then park it. + // Eventually it will be awoken once the engine + // exits the engine context + while !running_command.load(std::sync::atomic::Ordering::Relaxed) { + std::thread::park(); + } + }); + } + }); + + *SAFEPOINT_HANDLER.lock().unwrap() = Some(Arc::new(SafepointHandler { + running_command, + handle: safepoint_wakeup, + })); + + *INTERRUPT_HANDLER.lock().unwrap() = Some(Arc::new(InterruptHandler { + controller: controller.clone(), + running: running.clone(), + handle: thread_handle, + })); + + configure_engine_impl(engine) +} + +// The Steel scripting engine instance. This is what drives the whole integration. +pub static GLOBAL_ENGINE: Lazy> = + Lazy::new(|| Mutex::new(setup())); + +static GENERATION: AtomicUsize = AtomicUsize::new(0); + +fn increment_generation() { + GENERATION.fetch_add(1, std::sync::atomic::Ordering::SeqCst); +} + +fn is_current_generation(gen: usize) -> bool { + GENERATION.load(std::sync::atomic::Ordering::SeqCst) == gen +} + +fn load_generation() -> usize { + GENERATION.load(std::sync::atomic::Ordering::SeqCst) +} + +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, +{ + // Unpark the other thread, get it ready + let handler = SAFEPOINT_HANDLER.lock().unwrap().clone(); + if let Some(x) = &handler { + x.running_command + .store(true, std::sync::atomic::Ordering::Relaxed); + x.handle.thread().unpark(); + }; + + let res = f(&mut acquire_engine_lock()); + + if let Some(x) = handler { + x.running_command + .store(false, std::sync::atomic::Ordering::Relaxed); + x.handle.thread().unpark(); + }; + + res +} + +pub fn try_enter_engine(f: F) -> Option +where + F: FnOnce(&mut Engine) -> R, +{ + let handler = SAFEPOINT_HANDLER.lock().unwrap().clone().unwrap(); + + // If we're currently running a command, we need to try lock against + // the lock since we don't want to lock up the engine explicitly. + if handler + .running_command + .load(std::sync::atomic::Ordering::Relaxed) + { + let res = match GLOBAL_ENGINE.try_lock() { + Ok(mut v) => Some((f)(&mut v)), + Err(_) => None, + }; + + res + } else { + handler + .running_command + .store(true, std::sync::atomic::Ordering::Relaxed); + handler.handle.thread().unpark(); + + let res = match GLOBAL_ENGINE.lock() { + Ok(mut v) => Some((f)(&mut v)), + Err(_) => None, + }; + + handler + .running_command + .store(false, std::sync::atomic::Ordering::Relaxed); + handler.handle.thread().unpark(); + + res + } +} + +pub struct SafepointHandler { + running_command: Arc, + handle: std::thread::JoinHandle<()>, +} + +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.lock().unwrap().clone().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 +} + +static BUFFER_EXTENSION_KEYMAP: Lazy> = Lazy::new(|| { + RwLock::new(BufferExtensionKeyMap { + map: HashMap::new(), + reverse: HashMap::new(), + }) +}); + +fn reset_buffer_extension_keymap() { + let mut guard = BUFFER_EXTENSION_KEYMAP.write().unwrap(); + guard.map.clear(); + guard.reverse.clear(); +} + +enum LspKind { + Call(RootedSteelVal), + Notification(RootedSteelVal), +} + +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] +struct LspCallRegistryId { + lsp_name: String, + event_name: String, + generation: usize, +} + +struct LspCallRegistry { + map: HashMap, +} + +static LSP_CALL_REGISTRY: Lazy> = Lazy::new(|| { + RwLock::new(LspCallRegistry { + map: HashMap::new(), + }) +}); + +fn reset_lsp_call_registry() { + LSP_CALL_REGISTRY.write().unwrap().map.clear(); +} + +fn register_lsp_call_callback(lsp: String, kind: String, function: SteelVal) { + let rooted = function.as_rooted(); + + let id = LspCallRegistryId { + lsp_name: lsp, + event_name: kind, + generation: load_generation(), + }; + + LSP_CALL_REGISTRY + .write() + .unwrap() + .map + .insert(id, LspKind::Call(rooted)); +} + +fn register_lsp_notification_callback(lsp: String, kind: String, function: SteelVal) { + let rooted = function.as_rooted(); + + let id = LspCallRegistryId { + lsp_name: lsp, + event_name: kind, + generation: load_generation(), + }; + + LSP_CALL_REGISTRY + .write() + .unwrap() + .map + .insert(id, LspKind::Notification(rooted)); +} + +fn send_arbitrary_lsp_notification( + cx: &mut Context, + name: SteelString, + method: SteelString, + params: Option, +) -> anyhow::Result<()> { + let argument = params.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 language_server = cx + .editor + .language_server_by_id(language_server_id) + .ok_or(anyhow::anyhow!("Failed to find a language server by id"))?; + + // Send the notification using the custom method and arguments + language_server.send_custom_notification(method.to_string(), argument)?; + + Ok(()) +} + +pub struct BufferExtensionKeyMap { + map: HashMap, + reverse: HashMap, +} + +impl BufferExtensionKeyMap { + fn get_extension(&self, extension: &str) -> Option<&EmbeddedKeyMap> { + self.map.get(extension) + } + + fn get_doc_id(&self, id: usize) -> Option<&EmbeddedKeyMap> { + self.reverse.get(&id).and_then(|x| self.map.get(x)) + } +} + +pub fn get_extension_keymap() -> RwLockReadGuard<'static, BufferExtensionKeyMap> { + BUFFER_EXTENSION_KEYMAP.read().unwrap() +} + +fn add_extension_or_labeled_keymap(label: String, keymap: EmbeddedKeyMap) { + BUFFER_EXTENSION_KEYMAP + .write() + .unwrap() + .map + .insert(label, keymap); +} + +fn add_reverse_mapping(key: usize, label: String) { + BUFFER_EXTENSION_KEYMAP + .write() + .unwrap() + .reverse + .insert(key, label); +} + +fn load_component_api(engine: &mut Engine, generate_sources: bool) { + let module = helix_component_module(generate_sources); + + if generate_sources { + configure_lsp_builtins("component", &module); + } + + engine.register_module(module); +} + +fn load_keymap_api(engine: &mut Engine, generate_sources: bool) { + let mut module = BuiltInModule::new("helix/core/keymaps"); + + module.register_fn("helix-empty-keymap", empty_keymap); + module.register_fn("helix-default-keymap", default_keymap); + module.register_fn("helix-merge-keybindings", merge_keybindings); + module.register_fn("helix-string->keymap", string_to_embedded_keymap); + module.register_fn("keymap?", is_keymap); + module.register_fn("helix-deep-copy-keymap", deep_copy_keymap); + module.register_fn("query-keymap", query_keybindings); + + module.register_fn( + "#%add-extension-or-labeled-keymap", + add_extension_or_labeled_keymap, + ); + + module.register_fn("#%add-reverse-mapping", add_reverse_mapping); + + // This should be associated with a corresponding scheme module to wrap this up + module.register_fn("keymap-update-documentation!", update_documentation); + + if generate_sources { + configure_lsp_builtins("keymap", &module) + } + + engine.register_module(module); +} + +pub fn format_docstring(doc: &str) -> String { + let mut docstring = doc + .lines() + .map(|x| { + let mut line = ";;".to_string(); + line.push_str(x); + line.push('\n'); + line + }) + .collect::(); + + docstring.pop(); + + docstring +} + +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 docstring = format_docstring(doc); + + 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 docstring = format_docstring(doc); + + 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!( + "push-range-to-selection!", + push_range_to_selection, + "Push a new range to a selection. The new selection will be the primary one" + ); + function1!( + "set-current-selection-primary-index!", + set_selection_primary_index, + "Set the primary index of the current selection" + ); + function1!( + "remove-current-selection-range!", + remove_selection_range, + "Remove a range from the current selection" + ); + + 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!( + "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." + ); + + function1!( + "get-current-line-character", + current_line_character, + "Returns the current column number with the given position encoding" + ); + + let mut template_function_arity_0 = |name: &str, doc: &str| { + if generate_sources { + let docstring = format_docstring(doc); + + builtin_static_command_module.push_str(&format!( + r#" +(provide {}) +;;@doc +{} +(define ({}) + (helix.static.{} *helix.cx*)) +"#, + name, docstring, name, name + )); + } + }; + + macro_rules! function0 { + ($name:expr, $function:expr, $doc:expr) => {{ + module.register_fn($name, $function); + template_function_arity_0($name, $doc); + }}; + } + + function0!( + "cx->current-file", + current_path, + "Get the currently focused file path" + ); + + function0!( + "current_selection", + get_selection, + "Returns the current selection as a string" + ); + function0!( + "current-selection->string", + get_selection, + "Returns the current selection as a string" + ); + function0!("load-buffer!", load_buffer, "Evaluates the current buffer"); + function0!( + "current-highlighted-text!", + get_highlighted_text, + "Returns the currently highlighted text as a string" + ); + function0!( + "get-current-line-number", + current_line_number, + "Returns the current line number" + ); + function0!( + "get-current-column-number", + current_column_number, + "Returns the visual current column number of unicode graphemes" + ); + function0!( + "current-selection-object", + current_selection, + "Returns the current selection object" + ); + function0!( + "get-helix-cwd", + get_helix_cwd, + "Returns the current working directly that helix is using" + ); + function0!( + "move-window-far-left", + move_window_to_the_left, + "Moves the current window to the far left" + ); + function0!( + "move-window-far-right", + move_window_to_the_right, + "Moves the current window to the far right" + ); + + let mut template_function_no_context = |name: &str, doc: &str| { + if generate_sources { + let docstring = format_docstring(doc); + + builtin_static_command_module.push_str(&format!( + r#" +(provide {}) +;;@doc +{} +(define {} helix.static.{}) + "#, + name, docstring, name, name + )) + } + }; + + macro_rules! no_context { + ($name:expr, $function:expr, $doc:expr) => {{ + module.register_fn($name, $function); + template_function_no_context($name, $doc); + }}; + } + + no_context!( + "selection->primary-index", + |sel: Selection| sel.primary_index(), + "Returns index of the primary selection" + ); + no_context!( + "selection->primary-range", + |sel: Selection| sel.primary(), + "Returns the range for primary selection" + ); + no_context!( + "selection->ranges", + |sel: Selection| sel.ranges().to_vec(), + "Returns all ranges of the selection" + ); + no_context!( + "range-anchor", + |range: Range| range.anchor, + "Get the anchor of the range: the side that doesn't move when extending." + ); + no_context!( + "range->from", + |range: Range| range.from(), + "Get the start of the range" + ); + no_context!( + "range-head", + |range: Range| range.head, + "Get the head of the range, moved when extending." + ); + no_context!( + "range->to", + |range: Range| range.to(), + "Get the end of the range" + ); + no_context!( + "range->span", + |range: Range| (range.from(), range.to()), + "Get the span of the range (from, to)" + ); + + no_context!( + "range", + Range::new, + r#"Construct a new range object + +```scheme +(range anchor head) -> Range? +``` + "# + ); + no_context!( + "range->selection", + |range: Range| Selection::from(range), + "Convert a range into a selection" + ); + + 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", + "Returns the path to the helix.scm file as a string", + ); + template_function_no_context( + "get-init-scm-path", + "Returns the path to the init.scm file as a string", + ); + + if generate_sources { + if let Some(mut target_directory) = alternative_runtime_search_path() { + if !target_directory.exists() { + std::fs::create_dir_all(&target_directory).unwrap(); + } + + target_directory.push("static.scm"); + + std::fs::write(target_directory, &builtin_static_command_module).unwrap(); + } + + engine.register_steel_module( + "helix/static.scm".to_string(), + builtin_static_command_module, + ); + } + + if generate_sources { + configure_lsp_builtins("static", &module); + } + + engine.register_module(module); +} + +fn goto_line_impl(cx: &mut Context, mut line: usize, extend: bool) { + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + + if line > text.len_lines() { + line = text.len_lines(); + } + + let line = line.saturating_sub(1); + + let selection = doc.selection(view.id).clone().transform(|range| { + let line_start = text.line_to_char(line); + range.put_cursor(text, line_start, extend) + }); + crate::commands::push_jump(view, doc); + doc.set_selection(view.id, selection); +} + +fn goto_column_impl(cx: &mut Context, char_index: usize, extend: bool) { + let count = cx.count(); + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + let selection = doc.selection(view.id).clone().transform(|range| { + let line = range.cursor_line(text); + let line_start = text.line_to_char(line) + char_index; + let line_end = helix_core::line_ending::line_end_char_index(&text, line); + let pos = graphemes::nth_next_grapheme_boundary(text, line_start, count - 1).min(line_end); + range.put_cursor(text, pos, extend) + }); + crate::commands::push_jump(view, doc); + doc.set_selection(view.id, selection); +} + +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 { + let func = move |cx: &mut Context, args: Vec>| { + let mut cx = compositor::Context { + editor: cx.editor, + scroll: None, + jobs: cx.jobs, + }; + + let mut verified_args = Args::new(command.signature, true); + for arg in args { + verified_args.push(arg)?; + } + + verified_args + .finish() + .map_err(|e| anyhow::Error::msg(e.to_string()))?; + + (command.fun)(&mut cx, verified_args, 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, + format_docstring(command.doc), + command.name, + command.name + )); + } + } + + module.register_fn("goto-column", goto_column_impl); + module.register_fn("goto-line", goto_line_impl); + + builtin_typable_command_module.push_str( + &r#" +(provide goto-column) + +;;@doc +;; Move the cursor to the given character index within the same line +(define (goto-column col [extend #false]) + (helix.goto-column *helix.cx* col extend)) +"#, + ); + + builtin_typable_command_module.push_str( + &r#" +(provide goto-line) + +;;@doc +;; Move the cursor to the given line +(define (goto-line line [extend #false]) + (helix.goto-line *helix.cx* line extend)) +"#, + ); + + if generate_sources { + if let Some(mut target_directory) = alternative_runtime_search_path() { + if !target_directory.exists() { + std::fs::create_dir_all(&target_directory).unwrap(); + } + + target_directory.push("commands.scm"); + + std::fs::write(target_directory, &builtin_typable_command_module).unwrap(); + } + + engine.register_steel_module( + "helix/commands.scm".to_string(), + builtin_typable_command_module, + ); + } + + 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()) +} + +// Indent guides configurations +fn ig_render(config: &mut IndentGuidesConfig, option: bool) { + config.render = option; +} + +fn ig_character(config: &mut IndentGuidesConfig, option: char) { + config.character = option; +} + +fn ig_skip_levels(config: &mut IndentGuidesConfig, option: u8) { + config.skip_levels = option; +} + +// Whitespace configurations +fn ws_visible(config: &mut WhitespaceConfig, option: bool) { + let value = if option { + WhitespaceRenderValue::All + } else { + WhitespaceRenderValue::None + }; + config.render = WhitespaceRender::Basic(value); +} + +fn ws_chars(config: &mut WhitespaceConfig, option: HashMap) -> anyhow::Result<()> { + for (k, v) in option { + match k { + SteelVal::StringV(s) | SteelVal::SymbolV(s) => match s.as_str() { + "space" => config.characters.space = v, + "tab" => config.characters.tab = v, + "nbsp" => config.characters.nbsp = v, + "nnbsp" => config.characters.nnbsp = v, + "newline" => config.characters.newline = v, + "tabpad" => config.characters.tabpad = v, + unknown => anyhow::bail!("Unrecognized key: {}", unknown), + }, + other => anyhow::bail!("Unrecognized key option: {}", other), + } + } + Ok(()) +} + +fn ws_render(config: &mut WhitespaceConfig, option: HashMap) -> anyhow::Result<()> { + #[derive(Default)] + struct RenderFlags { + space: Option, + tab: Option, + nbsp: Option, + nnbsp: Option, + newline: Option, + default: Option, + } + + let mut base = match config.render { + WhitespaceRender::Basic(v) => RenderFlags { + default: Some(v.clone()), + space: Some(v.clone()), + nbsp: Some(v.clone()), + nnbsp: Some(v.clone()), + tab: Some(v.clone()), + newline: Some(v.clone()), + }, + WhitespaceRender::Specific { .. } => RenderFlags::default(), + }; + + for (k, v) in option { + let value = if v { + WhitespaceRenderValue::All + } else { + WhitespaceRenderValue::None + }; + match k { + SteelVal::StringV(s) | SteelVal::SymbolV(s) => match s.as_str() { + "space" => base.space = Some(value), + "tab" => base.tab = Some(value), + "nbsp" => base.nbsp = Some(value), + "nnbsp" => base.nnbsp = Some(value), + "newline" => base.newline = Some(value), + "default" => base.default = Some(value), + unknown => anyhow::bail!("Unrecognized key: {}", unknown), + }, + unknown => anyhow::bail!("Unrecognized key: {}", unknown), + } + } + + config.render = WhitespaceRender::Specific { + default: base.default, + space: base.space, + nbsp: base.nbsp, + nnbsp: base.nnbsp, + tab: base.tab, + newline: base.newline, + }; + + Ok(()) +} + +// 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; +} + +// Attempt to fuss with the configuration? +fn dynamic_set_option( + configuration: &HelixConfiguration, + key: String, + value: SteelVal, +) -> anyhow::Result<()> { + let key = key.to_lowercase(); + + let key_error = || anyhow::anyhow!("Unknown key `{}`", key); + + let mut config = serde_json::json!(configuration.load_config().editor); + let pointer = format!("/{}", key.replace('.', "/")); + let jvalue = config.pointer_mut(&pointer).ok_or_else(key_error)?; + + let cloned = value.clone(); + let field_error = move |_| anyhow::anyhow!("Could not parse field `{}`", cloned); + *jvalue = serde_json::Value::try_from(value)?; + + let config = serde_json::from_value(config).map_err(field_error)?; + + let mut new_config = configuration.load_config(); + new_config.editor = config; + + configuration.store_config(new_config); + + Ok(()) +} + +fn load_configuration_api(engine: &mut Engine, generate_sources: bool) { + let mut module = BuiltInModule::new("helix/core/configuration"); + + module.register_fn( + "register-lsp-notification-handler", + register_lsp_notification_callback, + ); + + module.register_fn("register-lsp-call-handler", register_lsp_call_callback); + + 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( + "get-lsp-config", + HelixConfiguration::get_language_server_config, + ); + + module.register_fn( + "set-lsp-config!", + HelixConfiguration::update_language_server_config, + ); + + module.register_fn( + "update-language-config!", + HelixConfiguration::update_language_config, + ); + + module.register_fn( + "refresh-all-language-configs!", + update_configuration_for_all_open_documents, + ); + + module + .register_fn("raw-cursor-shape", CursorShapeConfig::default) + .register_fn( + "raw-cursor-shape-set!", + |value: SteelVal, mode: String, shape: String| -> anyhow::Result { + let mut config = CursorShapeConfig::as_mut_ref(&value)?; + + let mode = match mode.as_str() { + "normal" => Mode::Normal, + "select" => Mode::Select, + "insert" => Mode::Insert, + _ => anyhow::bail!("Unable to match mode from string: {}", mode), + }; + + let kind = match shape.as_str() { + "block" => CursorKind::Block, + "bar" => CursorKind::Bar, + "underline" => CursorKind::Underline, + "hidden" => CursorKind::Hidden, + _ => anyhow::bail!("Unable to match cursor kind from string: {}", shape), + }; + + config.update(mode, kind); + drop(config); + Ok(value) + }, + ); + + 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("raw-whitespace", || WhitespaceConfig::default()) + .register_fn("register-whitespace", HelixConfiguration::whitespace) + .register_fn("ws-visible", ws_visible) + .register_fn("ws-chars", ws_chars) + .register_fn("ws-render", ws_render); + + module + .register_fn("raw-indent-guides", || IndentGuidesConfig::default()) + .register_fn("register-indent-guides", HelixConfiguration::indent_guides) + .register_fn("ig-render", ig_render) + .register_fn("ig-character", ig_character) + .register_fn("ig-skip-levels", ig_skip_levels); + + 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( + "jump-label-alphabet", + HelixConfiguration::jump_label_alphabet, + ) + .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) + .register_fn( + "#%editor-auto-pairs", + |ctx: &mut Context, auto_pairs: AutoPairConfig| { + ctx.editor.auto_pairs = auto_pairs.into(); + }, + ) + // 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-other-lines-enable", + HelixConfiguration::inline_diagnostics_other_lines_enable, + ) + .register_fn( + "inline-diagnostics-end-of-line-enable", + HelixConfiguration::inline_diagnostics_end_of_line_enable, + ) + .register_fn( + "inline-diagnostics-min-diagnostics-width", + HelixConfiguration::inline_diagnostics_min_diagnostic_width, + ) + .register_fn( + "inline-diagnostics-prefix-len", + HelixConfiguration::inline_diagnostics_prefix_len, + ) + .register_fn( + "inline-diagnostics-max-wrap", + HelixConfiguration::inline_diagnostics_max_wrap, + ) + .register_fn( + "inline-diagnostics-max-diagnostics", + HelixConfiguration::inline_diagnostics_max_diagnostics, + ) + .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("#%raw-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("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("bufferline", HelixConfiguration::bufferline) + .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) + .register_fn("rainbow-brackets", HelixConfiguration::rainbow_brackets); + + // Keybinding stuff + module + .register_fn("keybindings", HelixConfiguration::keybindings) + .register_fn("get-keybindings", HelixConfiguration::get_keybindings) + .register_fn("set-keybindings!", HelixConfiguration::set_keybindings) + .register_fn("set-option!", dynamic_set_option); + + if generate_sources { + let mut builtin_configuration_module = + r#"(require-builtin helix/core/configuration as helix.) + +(provide statusline) + +;;@doc +;; Configuration of the statusline elements. +;; The following status line elements can be configured: +;; +;; Key Description +;; ------------------------------------------------------------------------------------------- +;; mode The current editor mode (mode.normal/mode.insert/mode.select) +;; spinner A progress spinner indicating LSP activity +;; file-name The path/name of the opened file +;; file-absolute-path The absolute path/name of the opened file +;; file-base-name The basename of the opened file +;; file-modification-indicator The indicator to show whether the file is modified (a [+] appears when there are unsaved changes) +;; file-encoding The encoding of the opened file if it differs from UTF-8 +;; file-line-ending The file line endings (CRLF or LF) +;; file-indent-style The file indentation style +;; read-only-indicator An indicator that shows [readonly] when a file cannot be written +;; total-line-numbers The total line numbers of the opened file +;; file-type The type of the opened file +;; diagnostics The number of warnings and/or errors +;; workspace-diagnostics The number of warnings and/or errors on workspace +;; selections The primary selection index out of the number of active selections +;; primary-selection-length The number of characters currently in primary selection +;; position The cursor position +;; position-percentage The cursor position as a percentage of the total number of lines +;; separator The string defined in editor.statusline.separator (defaults to "│") +;; spacer Inserts a space between elements (multiple/contiguous spacers may be specified) +;; version-control The current branch name or detached commit hash of the opened workspace +;; register The current selected register +(define (statusline #:left [left (list "mode" "spinner" "file-name" "read-only-indicator" "file-modification-indicator")] + #:center [center '()] + #:right [right (list "diagnostics" "selections" "register" "position" "file-encoding")] + #:separator [separator "|"] + #:mode-normal [mode-normal "NOR"] + #:mode-insert [mode-insert "INS"] + #:mode-select [mode-select "SEL"] + #:diagnostics [diagnostics (list "warning" "error")] + #:workspace-diagnostics [workspace-diagnostics (list "warning" "error")]) + (helix.statusline *helix.config* + (hash 'left left + 'center center + 'right right + 'separator separator + 'mode-normal mode-normal + 'mode-insert mode-insert + 'mode-select mode-select + 'diagnostics diagnostics + 'workspace-diagnostics workspace-diagnostics))) + +(provide indent-heuristic) +;;@doc +;; Which indent heuristic to use when a new line is inserted +;; Defaults to `"hybrid"` +;; Valid options are: +;; * "simple" +;; * "tree-sitter" +;; * "hybrid" +(define (indent-heuristic kind) + (set-option! 'indent-heuristic kind)) + +(provide atomic-save) + +;;@doc +;; Whether to use atomic operations to write documents to disk. +;; This prevents data loss if the editor is interrupted while writing the file, but may +;; confuse some file watching/hot reloading programs. Defaults to `#true`. +(define (atomic-save bool-opt) + (set-option! 'atomic-save bool-opt)) + +(provide lsp) + +;;@doc +;; Blanket LSP configuration +;; The options are provided in a hashmap, and provided options will be merged +;; with the defaults. The options are as follows: +;; +;; Enables LSP +;; * enable: bool +;; +;; Display LSP messagess from $/progress below statusline +;; * display-progress-messages: bool +;; +;; Display LSP messages from window/showMessage below statusline +;; * display-messages: bool +;; +;; Enable automatic pop up of signature help (parameter hints) +;; * auto-signature-help: bool +;; +;; Display docs under signature help popup +;; * display-signature-help-docs: bool +;; +;; Display inlay hints +;; * display-inlay-hints: bool +;; +;; Maximum displayed length of inlay hints (excluding the added trailing `…`). +;; If it's `None`, there's no limit +;; * inlay-hints-length-limit: Option +;; +;; Display document color swatches +;; * display-color-swatches: bool +;; +;; Whether to enable snippet support +;; * snippets: bool +;; +;; Whether to include declaration in the goto reference query +;; * goto_reference_include_declaration: bool +;; +;;```scheme +;; (lsp (hash 'display-inlay-hints #t)) +;;``` +;; +;; The defaults shown from the rust side are as follows: +;; ```rust +;; LspConfig { +;; enable: true, +;; display_progress_messages: false, +;; display_messages: true, +;; auto_signature_help: true, +;; display_signature_help_docs: true, +;; display_inlay_hints: false, +;; inlay_hints_length_limit: None, +;; snippets: true, +;; goto_reference_include_declaration: true, +;; display_color_swatches: true, +;; } +;; +;; ``` +(define (lsp configuration) + (helix.lsp *helix.config* configuration)) + +(provide search) + +;;@doc +;; Search configuration +;; Accepts two keywords, #:smart-case and #:wrap-around, both default to true. +;; +;; ```scheme +;; (search #:smart-case #t #:wrap-around #t) +;; (search #:smart-case #f #:wrap-around #f) +;; ``` +(define (search #:smart-case [smart-case #t] #:wrap-around [wrap-around #true]) + (helix.search *helix.config* smart-case wrap-around)) + +(provide auto-pairs) + +;;@doc +;; Automatic insertion of pairs to parentheses, brackets, +;; etc. Optionally, this can be a list of pairs to specify a +;; global list of characters to pair, or a hashmap of character to character. +;; Defaults to true. +;; +;; ```scheme +;; (auto-pairs #f) +;; (auto-pairs #t) +;; (auto-pairs (list '(#\{ . #\}))) +;; (auto-pairs (list '(#\{ #\}))) +;; (auto-pairs (list (cons #\{ #\}))) +;; (auto-pairs (hash #\{ #\})) +;; ``` +(define (auto-pairs bool-or-map-or-pairs) + (when (hash? bool-or-map-or-pairs) + (helix.auto-pairs *helix.config* (helix.auto-pairs-map bool-or-map-or-pairs)) + (helix.#%editor-auto-pairs *helix.cx* (helix.auto-pairs-map bool-or-map-or-pairs))) + + (when (bool? bool-or-map-or-pairs) + (helix.auto-pairs *helix.config* (helix.auto-pairs-default bool-or-map-or-pairs)) + (helix.#%editor-auto-pairs *helix.cx* (helix.auto-pairs-default bool-or-map-or-pairs))) + + (when (list? bool-or-map-or-pairs) + (helix.auto-pairs *helix.config* + (helix.auto-pairs-map + (#%prim.transduce bool-or-map-or-pairs + (into-hashmap)))) + + (helix.#%editor-auto-pairs *helix.cx* + (helix.auto-pairs-map + (#%prim.transduce bool-or-map-or-pairs + (into-hashmap)))))) + +(provide continue-comments) + +;;@doc +;; Whether comments should be continued. +(define (continue-comments bool) + (set-option! 'continue-comments bool)) + +(provide popup-border) + +;;@doc +;; Set the popup border. +;; Valid options are: +;; * "none" +;; * "all" +;; * "popup" +;; * "menu" +(define (popup-border option) + (set-option! 'popup-border option)) + +(provide register-lsp-notification-handler) + +;;@doc +;; Register a callback to be called on LSP notifications sent from the server -> client +;; that aren't currently handled by Helix as a built in. +;; +;; ```scheme +;; (register-lsp-notification-handler lsp-name event-name handler) +;; ``` +;; +;; * lsp-name : string? +;; * event-name : string? +;; * function : (-> hash? any?) ;; Function where the first argument is the parameters +;; +;; # Examples +;; ``` +;; (register-lsp-notification-handler "dart" +;; "dart/textDocument/publishClosingLabels" +;; (lambda (args) (displayln args))) +;; ``` +(define register-lsp-notification-handler helix.register-lsp-notification-handler) + +(provide register-lsp-call-handler) + +;;@doc +;; Register a callback to be called on LSP calls sent from the server -> client +;; that aren't currently handled by Helix as a built in. +;; +;; ```scheme +;; (register-lsp-call-handler lsp-name event-name handler) +;; ``` +;; +;; * lsp-name : string? +;; * event-name : string? +;; * function : (-> hash? any?) ;; Function where the first argument is the parameters +;; +;; # Examples +;; ``` +;; (register-lsp-call-handler "dart" +;; "dart/textDocument/publishClosingLabels" +;; (lambda (call-id args) (displayln args))) +;; ``` +(define register-lsp-call-handler helix.register-lsp-call-handler) + + +;;@doc +;; Set a configuration option by key name. +(provide set-option!) +(define (set-option! key value) + (helix.set-option! *helix.config* key value)) + +(provide define-lsp) + +;;@doc +;; Syntax: +;; +;; Registers an lsp configuration. This is a thin wrapper around passing +;; a hashmap manually to `set-lsp-config!`, and has a slightly more elegant +;; API. +;; +;; Examples: +;; ```scheme +;; (define-lsp "steel-language-server" (command steel-language-server) (args '())) +;; (define-lsp "rust-analyzer" (config (experimental (hash 'testExplorer #t 'runnables '("cargo"))))) +;; (define-lsp "tinymist" (config (exportPdf "onType") (outputPath "$root/$dir/$name"))) +;; ``` +(define-syntax define-lsp + (syntax-rules (#%crunch #%name #%conf) + ;; Other generic keys + [(_ #%crunch #%name name #%conf conf (key (inner-key value) ...)) + (set-lsp-config! name + (hash-insert conf + (quote key) + (transduce (list (list (quote inner-key) value) ...) + (into-hashmap))))] + + [(_ #%crunch #%name name #%conf conf (key (inner-key value) ...) remaining ...) + ; ;; Crunch the remaining stuff + (define-lsp #%crunch + #%name + name + #%conf + (hash-insert conf + (quote key) + (transduce (list (list (quote inner-key) value) ...) (into-hashmap))) + remaining ...)] + + ;; Other generic keys + [(_ #%crunch #%name name #%conf conf (key value)) + (set-lsp-config! name (hash-insert conf (quote key) value))] + + [(_ #%crunch #%name name #%conf conf (key value) remaining ...) + ; ;; Crunch the remaining stuff + (define-lsp #%crunch #%name name #%conf (hash-insert conf (quote key) value) remaining ...)] + + [(_ name (key value ...) ...) + (define-lsp #%crunch #%name name #%conf (hash "name" name) (key value ...) ...)] + + [(_ name (key value)) (define-lsp #%crunch #%name name #%conf (hash "name" name) (key value))] + + [(_ name (key value) ...) (define-lsp #%crunch #%name name #%conf (hash "name" name) (key value) ...)])) + +(provide define-language) + +;;@doc +;; Syntax: +;; +;; Defines a language configuration. +;; This is a thin wrapper around calling `update-language-config!` with a hash +;; of arguments, and has a slightly more elegant syntax. +;; +;; ```scheme +;; (define-language "scheme" +;; (formatter (command "raco") (args '("fmt" "-i"))) +;; (auto-format #true) +;; (language-servers '("steel-language-server"))) +;; +;; ``` +(define-syntax define-language + (syntax-rules (#%crunch #%name #%conf) + + ;; Other generic keys + [(_ #%crunch #%name name #%conf conf (key (inner-key value) ...)) + (update-language-config! name + (hash-insert conf + (quote key) + (transduce (list (list (quote inner-key) value) ...) + (into-hashmap))))] + + [(_ #%crunch #%name name #%conf conf (key (inner-key value) ...) remaining ...) + ; ;; Crunch the remaining stuff + (define-language #%crunch + #%name + name + #%conf + (hash-insert conf + (quote key) + (transduce (list (list (quote inner-key) value) ...) (into-hashmap))) + remaining ...)] + + ;; Other generic keys + [(_ #%crunch #%name name #%conf conf (key value)) + (update-language-config! name (hash-insert conf (quote key) value))] + + [(_ #%crunch #%name name #%conf conf (key value) remaining ...) + ; ;; Crunch the remaining stuff + (define-language #%crunch #%name name #%conf (hash-insert conf (quote key) value) remaining ...)] + + [(_ name (key value ...) ...) + (define-language #%crunch #%name name #%conf (hash "name" name) (key value ...) ...)] + + [(_ name (key value)) (language #%crunch #%name name #%conf (hash "name" name) (key value))] + + [(_ name (key value) ...) + (define-language #%crunch #%name name #%conf (hash "name" name) (key value) ...)])) +"# + .to_string(); + + builtin_configuration_module.push_str( + r#" +(provide cursor-shape) +;;@doc +;; Shape for cursor in each mode +;; +;; (cursor-shape #:normal (normal 'block) +;; #:select (select 'block) +;; #:insert (insert 'block)) +;; +;; # Examples +;; +;; ```scheme +;; (cursor-shape #:normal 'block #:select 'underline #:insert 'bar) +;; ``` +(define (cursor-shape #:normal (normal 'block) + #:select (select 'block) + #:insert (insert 'block)) + (define cursor-shape-config (helix.raw-cursor-shape)) + (helix.raw-cursor-shape-set! cursor-shape-config 'normal normal) + (helix.raw-cursor-shape-set! cursor-shape-config 'select select) + (helix.raw-cursor-shape-set! cursor-shape-config 'insert insert) + (helix.#%raw-cursor-shape *helix.config* cursor-shape-config)) + "#, + ); + + builtin_configuration_module.push_str( + r#" +(provide refresh-all-language-configs!) +(define (refresh-all-language-configs!) + (helix.refresh-all-language-configs! *helix.cx*)) + "#, + ); + + builtin_configuration_module.push_str( + r#" +(provide update-configuration!) +(define (update-configuration!) + (helix.update-configuration! *helix.config*)) +"#, + ); + + builtin_configuration_module.push_str( + 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( + r#" +(provide set-configuration-for-file!) +(define (set-configuration-for-file! path config) + (helix.set-configuration-for-file! *helix.cx* path config)) +"#, + ); + + builtin_configuration_module.push_str( + r#" +(provide get-lsp-config) + +;;@doc +;; Get the lsp configuration for a language server. +;; +;; Returns a hashmap which can be passed to `set-lsp-config!` +(define (get-lsp-config lsp) + (helix.get-lsp-config *helix.config* lsp)) + "#, + ); + + builtin_configuration_module.push_str( + r#" +(provide set-lsp-config!) +;;@doc +;; Sets the language server config for a specific language server. +;; +;; ```scheme +;; (set-lsp-config! lsp config) +;; ``` +;; * lsp : string? +;; * config: hash? +;; +;; This will overlay the existing configuration, much like the existing +;; toml definition does. +;; +;; Available options for the config hash are: +;; ```scheme +;; (hash "command" "" +;; "args" (list "args" ...) +;; "environment" (hash "ENV" "VAR" ...) +;; "config" (hash ...) +;; "timeout" 100 ;; number +;; "required-root-patterns" (listof "pattern" ...)) +;; +;; ``` +;; +;; # Examples +;; ``` +;; (set-lsp-config! "jdtls" +;; (hash "args" (list "-data" "/home/matt/code/java-scratch/workspace"))) +;; ``` +(define (set-lsp-config! lsp config) + (helix.set-lsp-config! *helix.config* lsp config)) +"#, + ); + + builtin_configuration_module.push_str( + r#" +(provide update-language-config!) +(define (update-language-config! lsp config) + (helix.update-language-config! *helix.config* lsp config) + (refresh-all-language-configs!)) +"#, + ); + + // Register the get keybindings function + builtin_configuration_module.push_str( + r#" +(provide get-keybindings) +(define (get-keybindings) + (helix.get-keybindings *helix.config*)) +"#, + ); + + let mut template_whitespace = |name: &str| { + builtin_configuration_module.push_str(&format!( + r#" +(provide {}) +(define ({} arg) + (lambda (picker) + (helix.{} picker arg) + picker)) +"#, + name, name, name + )) + }; + let whitespace_functions = &["ws-visible", "ws-chars", "ws-render"]; + + for name in whitespace_functions { + template_whitespace(name); + } + + let mut template_indent_guides = |name: &str| { + builtin_configuration_module.push_str(&format!( + r#" +(provide {}) +(define ({} arg) + (lambda (picker) + (helix.{} picker arg) + picker)) +"#, + name, name, name + )) + }; + let indent_guides_functions = &["ig-render", "ig-character", "ig-skip-levels"]; + + for name in indent_guides_functions { + template_indent_guides(name); + } + + 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( + r#" +(provide file-picker-kw) +;;@doc +;; Sets the configuration for the file picker using keywords. +;; +;; ```scheme +;; (file-picker-kw #:hidden #t +;; #:follow-symlinks #t +;; #:deduplicate-links #t +;; #:parents #t +;; #:ignore #t +;; #:git-ignore #t +;; #:git-exclude #t +;; #:git-global #t +;; #:max-depth #f) ;; Expects either #f or an int? +;; ``` +;; By default, max depth is `#f` while everything else is an int? +;; +;; To use this, call this in your `init.scm` or `helix.scm`: +;; +;; # Examples +;; ```scheme +;; (file-picker-kw #:hidden #f) +;; ``` +(define (file-picker-kw + #:hidden [hidden #t] + #:follow-symlinks [follow-symlinks #t] + #:deduplicate-links [deduplicate-links #t] + #:parents [parents #t] + #:ignore [ignore #t] + #:git-ignore [git-ignore #t] + #:git-global [git-global #t] + #:git-exclude [git-exclude #t] + #:max-depth [max-depth #f]) + + (define picker (helix.raw-file-picker)) + (unless hidden (helix.fp-hidden picker hidden)) + (unless follow-symlinks (helix.fp-follow-symlinks picker follow-symlinks)) + (unless deduplicate-links (helix.fp-deduplicate-links picker deduplicate-links)) + (unless parents (helix.fp-parents picker parents)) + (unless ignore (helix.fp-ignore picker ignore)) + (unless git-ignore (helix.fp-git-ignore picker git-ignore)) + (unless git-global (helix.fp-git-global picker git-global)) + (unless git-exclude (helix.fp-git-exclude picker git-exclude)) + (when max-depth (helix.fp-max-depth picker max-depth)) + (helix.register-file-picker *helix.config* picker)) + "#, + ); + + builtin_configuration_module.push_str( + r#" +(provide file-picker) +;;@doc +;; Sets the configuration for the file picker using var args. +;; +;; ```scheme +;; (file-picker . args) +;; ``` +;; +;; The args are expected to be something of the value: +;; ```scheme +;; (-> FilePickerConfiguration? bool?) +;; ``` +;; +;; These other functions in this module which follow this behavior are all +;; prefixed `fp-`, and include: +;; +;; * fp-hidden +;; * fp-follow-symlinks +;; * fp-deduplicate-links +;; * fp-parents +;; * fp-ignore +;; * fp-git-ignore +;; * fp-git-global +;; * fp-git-exclude +;; * fp-max-depth +;; +;; By default, max depth is `#f` while everything else is an int? +;; +;; To use this, call this in your `init.scm` or `helix.scm`: +;; +;; # Examples +;; ```scheme +;; (file-picker (fp-hidden #f) (fp-parents #f)) +;; ``` +(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( + r#" +(provide soft-wrap-kw) +;;@doc +;; Sets the configuration for soft wrap using keyword args. +;; +;; ```scheme +;; (soft-wrap-kw #:enable #f +;; #:max-wrap 20 +;; #:max-indent-retain 40 +;; #:wrap-indicator "↪" +;; #:wrap-at-text-width #f) +;; ``` +;; +;; The options are as follows: +;; +;; * #:enable: +;; Soft wrap lines that exceed viewport width. Default to off +;; * #:max-wrap: +;; Maximum space left free at the end of the line. +;; This space is used to wrap text at word boundaries. If that is not possible within this limit +;; the word is simply split at the end of the line. +;; +;; This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views. +;; +;; Default to 20 +;; * #:max-indent-retain +;; Maximum number of indentation that can be carried over from the previous line when softwrapping. +;; If a line is indented further then this limit it is rendered at the start of the viewport instead. +;; +;; This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views. +;; +;; Default to 40 +;; * #:wrap-indicator +;; Indicator placed at the beginning of softwrapped lines +;; +;; Defaults to ↪ +;; * #:wrap-at-text-width +;; Softwrap at `text_width` instead of viewport width if it is shorter +;; +;; # Examples +;; ```scheme +;; (soft-wrap-kw #:sw-enable #t) +;; ``` +(define (soft-wrap-kw #:enable [enable #f] + #:max-wrap [max-wrap 20] + #:max-indent-retain [max-indent-retain 40] + #:wrap-indicator [wrap-indicator 4] + #:wrap-at-text-width [wrap-at-text-width #f]) + (define sw (helix.raw-soft-wrap)) + (helix.sw-enable sw enable) + (helix.sw-max-wrap sw max-wrap) + (helix.sw-max-indent-retain sw max-indent-retain) + (helix.sw-wrap-indicator sw wrap-indicator) + (helix.sw-wrap-at-text-width sw wrap-at-text-width) + (helix.register-soft-wrap *helix.config* sw)) +"#, + ); + + builtin_configuration_module.push_str( + r#" + +(provide soft-wrap) +;;@doc +;; Sets the configuration for soft wrap using var args. +;; +;; ```scheme +;; (soft-wrap . args) +;; ``` +;; +;; The args are expected to be something of the value: +;; ```scheme +;; (-> SoftWrapConfiguration? bool?) +;; ``` +;; The options are as follows: +;; +;; * sw-enable: +;; Soft wrap lines that exceed viewport width. Default to off +;; * sw-max-wrap: +;; Maximum space left free at the end of the line. +;; This space is used to wrap text at word boundaries. If that is not possible within this limit +;; the word is simply split at the end of the line. +;; +;; This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views. +;; +;; Default to 20 +;; * sw-max-indent-retain +;; Maximum number of indentation that can be carried over from the previous line when softwrapping. +;; If a line is indented further then this limit it is rendered at the start of the viewport instead. +;; +;; This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views. +;; +;; Default to 40 +;; * sw-wrap-indicator +;; Indicator placed at the beginning of softwrapped lines +;; +;; Defaults to ↪ +;; * sw-wrap-at-text-width +;; Softwrap at `text_width` instead of viewport width if it is shorter +;; +;; # Examples +;; ```scheme +;; (soft-wrap (sw-enable #t)) +;; ``` +(define (soft-wrap . args) + (helix.register-soft-wrap + *helix.config* + (foldl (lambda (func config) (func config)) (helix.raw-soft-wrap) args))) +"#, + ); + + builtin_configuration_module.push_str(&format!( + r#" + +(provide whitespace) +;;@doc +;; Sets the configuration for whitespace using var args. +;; +;; ```scheme +;; (whitespace . args) +;; ``` +;; +;; The args are expected to be something of the value: +;; ```scheme +;; (-> WhitespaceConfiguration? bool?) +;; ``` +;; The options are as follows: +;; +;; * ws-visible: +;; Show all visible whitespace, defaults to false +;; * ws-render: +;; manually disable or enable characters +;; render options (specified in hashmap): +;;```scheme +;; (hash +;; 'space #f +;; 'nbsp #f +;; 'nnbsp #f +;; 'tab #f +;; 'newline #f) +;;``` +;; * ws-chars: +;; manually set visible whitespace characters with a hashmap +;; character options (specified in hashmap): +;;```scheme +;; (hash +;; 'space #\· +;; 'nbsp #\⍽ +;; 'nnbsp #\␣ +;; 'tab #\→ +;; 'newline #\⏎ +;; ; Tabs will look like "→···" (depending on tab width) +;; 'tabpad #\·) +;;``` +;; # Examples +;; ```scheme +;; (whitespace (ws-visible #t) (ws-chars (hash 'space #\·)) (ws-render (hash 'tab #f))) +;; ``` +(define (whitespace . args) + (helix.register-whitespace + *helix.config* + (foldl (lambda (func config) (func config)) (helix.raw-whitespace) args))) +"#, + )); + + builtin_configuration_module.push_str(&format!( + r#" + +(provide indent-guides) +;;@doc +;; Sets the configuration for indent-guides using args +;; +;; ```scheme +;; (indent-guides . args) +;; ``` +;; +;; The args are expected to be something of the value: +;; ```scheme +;; (-> IndentGuidesConfig? bool?) +;; ``` +;; The options are as follows: +;; +;; * ig-render: +;; Show indent guides, defaults to false +;; * ig-character: +;; character used for indent guides, defaults to "╎" +;; * ig-skip-levels: +;; amount of levels to skip, defaults to 1 +;; +;; # Examples +;; ```scheme +;; (indent-guides (ig-render #t) (ig-character #\|) (ig-skip-levels 1)) +;; ``` +(define (indent-guides . args) + (helix.register-indent-guides + *helix.config* + (foldl (lambda (func config) (func config)) (helix.raw-indent-guides) args))) +"#, + )); + + let mut template_function_arity_1 = |name: &str, doc: &str| { + let doc = format_docstring(doc); + builtin_configuration_module.push_str(&format!( + r#" +(provide {}) +;;@doc +;;{} +(define ({} arg) + (helix.{} *helix.config* arg)) +"#, + name, doc, name, name + )); + }; + + let functions = &[ + ("scrolloff", "Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5."), + ("scroll_lines", "Number of lines to scroll at once. Defaults to 3 +"), + ("mouse", "Mouse support. Defaults to true."), + ("shell", r#"Shell to use for shell commands. Defaults to ["cmd", "/C"] on Windows and ["sh", "-c"] otherwise."#), + ("jump-label-alphabet", r#"The characters that are used to generate two character jump labels. Characters at the start of the alphabet are used first. Defaults to "abcdefghijklmnopqrstuvwxyz""#), + ("line-number", "Line number mode. Defaults to 'absolute, set to 'relative for relative line numbers"), + ("cursorline", "Highlight the lines cursors are currently on. Defaults to false"), + ("cursorcolumn", "Highlight the columns cursors are currently on. Defaults to false"), + ("middle-click-paste", "Middle click paste support. Defaults to true"), + ("auto-completion", "Automatic auto-completion, automatically pop up without user trigger. Defaults to true."), + // TODO: Put in path_completion + ("auto-format", "Automatic formatting on save. Defaults to true."), + ("auto-save", r#"Automatic save on focus lost and/or after delay. +Time delay in milliseconds since last edit after which auto save timer triggers. +Time delay defaults to false with 3000ms delay. Focus lost defaults to false. + "#), + ("text-width", "Set a global text_width"), + ("idle-timeout", r#"Time in milliseconds since last keypress before idle timers trigger. +Used for various UI timeouts. Defaults to 250ms."#), + ("completion-timeout", r#" +Time in milliseconds after typing a word character before auto completions +are shown, set to 5 for instant. Defaults to 250ms. + "#), + ("preview-completion-insert", "Whether to insert the completion suggestion on hover. Defaults to true."), + ("completion-trigger-len", "Length to trigger completions"), + ("completion-replace", r#"Whether to instruct the LSP to replace the entire word when applying a completion + or to only insert new text +"#), + ("auto-info", "Whether to display infoboxes. Defaults to true."), + // ("cursor-shape", "Shape for cursor in each mode"), + ("true-color", "Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`."), + ("insert-final-newline", "Whether to automatically insert a trailing line-ending on write if missing. Defaults to `true`"), + ("color-modes", "Whether to color modes with different colors. Defaults to `false`."), + ("gutters", "Gutter configuration"), + ("undercurl", "Set to `true` to override automatic detection of terminal undercurl support in the event of a false negative. Defaults to `false`."), + ("terminal", "Terminal config"), + ("rulers", "Column numbers at which to draw the rulers. Defaults to `[]`, meaning no rulers"), + ("bufferline", "Persistently display open buffers along the top"), + ("workspace-lsp-roots", "Workspace specific lsp ceiling dirs"), + ("default-line-ending", "Which line ending to choose for new documents. Defaults to `native`. i.e. `crlf` on Windows, otherwise `lf`."), + ("smart-tab", "Enables smart tab"), + ("rainbow-brackets", "Enabled rainbow brackets"), + ("keybindings", "Keybindings config"), + ("set-keybindings!", "Override the global keybindings with the provided keymap"), + ("inline-diagnostics-cursor-line-enable", "Inline diagnostics cursor line"), + ("inline-diagnostics-other-lines-enable", "Inline diagnostics other lines"), + ("inline-diagnostics-end-of-line-enable", "Inline diagnostics end of line"), + ("inline-diagnostics-min-diagnostics-width", "Inline diagnostics min diagnostics width"), + ("inline-diagnostics-prefix-len", "Inline diagnostics prefix length"), + ("inline-diagnostics-max-wrap", "Inline diagnostics maximum wrap"), + ("inline-diagnostics-max-diagnostics", "Inline diagnostics max diagnostics"), + // language configuration functions + ("get-language-config", "Get the configuration for a specific language"), + // ("get-language-config-by-filename", "Get the language configuration for a specific file"), + ("set-language-config!", "Set the language configuration"), + ]; + + for (func, doc) in functions { + template_function_arity_1(func, doc); + } + + if let Some(mut target_directory) = alternative_runtime_search_path() { + if !target_directory.exists() { + std::fs::create_dir_all(&target_directory).unwrap(); + } + + target_directory.push("configuration.scm"); + + std::fs::write(target_directory, &builtin_configuration_module).unwrap(); + } + + engine.register_steel_module( + "helix/configuration.scm".to_string(), + builtin_configuration_module, + ); + } + + 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); +} + +fn load_high_level_keymap_api(engine: &mut Engine, generate_sources: bool) { + let keymap = include_str!("keymaps.scm"); + + if generate_sources { + if let Some(mut target_directory) = alternative_runtime_search_path() { + if !target_directory.exists() { + std::fs::create_dir_all(&target_directory).unwrap(); + } + + target_directory.push("keymaps.scm"); + + std::fs::write(target_directory, keymap).unwrap(); + } + } + + engine.register_steel_module("helix/keymaps.scm".to_string(), keymap.to_string()); +} + +fn load_high_level_theme_api(engine: &mut Engine, generate_sources: bool) { + let theme = include_str!("themes.scm"); + + if generate_sources { + if let Some(mut target_directory) = alternative_runtime_search_path() { + if !target_directory.exists() { + std::fs::create_dir_all(&target_directory).unwrap(); + } + + target_directory.push("themes.scm"); + + std::fs::write(target_directory, theme).unwrap(); + } + } + + engine.register_steel_module("helix/themes.scm".to_string(), theme.to_string()); +} + +#[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) { + Arc::make_mut(&mut cx.editor.theme_loader) + .add_dynamic_theme(theme.0.name().to_owned(), theme.0); +} + +fn get_style(theme: &SteelTheme, name: SteelString) -> helix_view::theme::Style { + theme.0.get(name.as_str()) +} + +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"); + + let mut builtin_editor_command_module = + "(require-builtin helix/core/editor as helix.)".to_string(); + + let mut template_function_arity_0 = |name: &str, doc: &str| { + let doc = format_docstring(doc); + builtin_editor_command_module.push_str(&format!( + r#" +(provide {}) +;;@doc +{} +(define ({}) + (helix.{} *helix.cx*)) +"#, + name, doc, name, name + )); + }; + + macro_rules! register_0 { + ($name:expr, $func:expr, $doc:expr) => { + module.register_fn($name, $func); + template_function_arity_0($name, $doc); + }; + } + + // 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 + register_0!( + "editor-focus", + cx_current_focus, + r#" +Get the current focus of the editor, as a `ViewId`. + +```scheme +(editor-focus) -> ViewId +``` + "# + ); + + register_0!( + "editor-mode", + cx_get_mode, + r#" +Get the current mode of the editor + +```scheme +(editor-mode) -> Mode? +``` + "# + ); + + register_0!( + "cx->themes", + get_themes, + "DEPRECATED: Please use `themes->list`" + ); + + register_0!( + "editor-count", + |cx: &mut Context| { cx.editor.count.map(|x| x.get()).unwrap_or(1) }, + "Get the count" + ); + + register_0!( + "themes->list", + get_themes, + r#" +Get the current themes as a list of strings. + +```scheme +(themes->list) -> (listof string?) +``` + "# + ); + + register_0!( + "editor-all-documents", + cx_editor_all_documents, + r#" +Get a list of all of the document ids that are currently open. + +```scheme +(editor-all-documents) -> (listof DocumentId?) +``` + "# + ); + register_0!( + "cx->cursor", + |cx: &mut Context| cx.editor.cursor(), + r#"DEPRECATED: Please use `current-cursor`"# + ); + + register_0!( + "current-cursor", + |cx: &mut Context| cx.editor.cursor(), + r#"Gets the primary cursor position in screen coordinates, +or `#false` if the primary cursor is not visible on screen. + +```scheme +(current-cursor) -> (listof? (or Position? #false) CursorKind) +``` + "# + ); + + register_0!( + "editor-focused-buffer-area", + current_buffer_area, + r#" +Get the `Rect` associated with the currently focused buffer. + +```scheme +(editor-focused-buffer-area) -> (or Rect? #false) +``` + "# + ); + register_0!( + "selected-register!", + |cx: &mut Context| cx + .editor + .selected_register + .unwrap_or(cx.editor.config().default_yank_register), + r#"Get currently selected register"# + ); + + // 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); + + // Get the last saved time of the document + module.register_fn( + "editor-document-last-saved", + |cx: &mut Context, doc: DocumentId| -> Option { + cx.editor.documents.get(&doc).map(|x| x.last_saved_time()) + }, + ); + + module.register_fn("editor-document->language", cx_get_document_language); + + module.register_fn( + "editor-document-dirty?", + |cx: &mut Context, doc: DocumentId| -> Option { + cx.editor.documents.get(&doc).map(|x| x.is_modified()) + }, + ); + + module.register_fn( + "editor-document-reload", + |cx: &mut Context, doc: DocumentId| -> anyhow::Result<()> { + for (view, _) in cx.editor.tree.views_mut() { + if let Some(x) = cx.editor.documents.get_mut(&doc) { + x.reload(view, &cx.editor.diff_providers)?; + } + } + Ok(()) + }, + ); + + module.register_fn("set-buffer-uri!", set_buffer_uri); + + module.register_fn("editor-doc-exists?", cx_document_exists); + + // Arity 2 + module.register_fn("editor-switch-action!", cx_switch_action); + module.register_fn( + "set-register!", + |cx: &mut Context, name: char, value: Vec| cx.editor.registers.write(name, value), + ); + + // Arity 1 + module.register_fn("editor->text", document_id_to_text); + module.register_fn("editor-document->path", document_path); + module.register_fn("register->value", cx_register_value); + + 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("string->editor-mode", string_to_mode); + + if generate_sources { + 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_1 = |name: &str, doc: &str| { + if generate_sources { + let docstring = format_docstring(doc); + builtin_editor_command_module.push_str(&format!( + r#" +(provide {}) +;;@doc +{} +(define ({} arg) + (helix.{} *helix.cx* arg)) +"#, + name, docstring, name, name + )); + } + }; + + template_function_arity_1( + "string->editor-mode", + r#" +Create an editor mode from a string, or false if it string was not one of +"normal", "insert", or "select" + +```scheme +(string->editor-mode "normal") -> (or Mode? #f) +``` + "#, + ); + + template_function_arity_1("editor->doc-id", "Get the document from a given view."); + template_function_arity_1("editor-switch!", "Open the document in a vertical split."); + template_function_arity_1("editor-set-focus!", "Set focus on the view."); + template_function_arity_1("editor-set-mode!", "Set the editor mode."); + template_function_arity_1( + "editor-doc-in-view?", + "Check whether the current view contains a document.", + ); + template_function_arity_1( + "set-scratch-buffer-name!", + "Set the name of a scratch buffer.", + ); + + // TODO: Lift this up + template_function_arity_1("set-buffer-uri!", "Set the URI of the buffer"); + template_function_arity_1("editor-doc-exists?", "Check if a document exists."); + + template_function_arity_1( + "editor-document-last-saved", + "Check when a document was last saved (returns a `SystemTime`)", + ); + + template_function_arity_1( + "editor-document->language", + "Get the language for the document", + ); + + template_function_arity_1( + "editor-document-dirty?", + "Check if a document has unsaved changes", + ); + + template_function_arity_1("editor-document-reload", "Reload a document."); + + template_function_arity_1("editor->text", "Get the document as a rope."); + template_function_arity_1("editor-document->path", "Get the path to a document."); + template_function_arity_1( + "register->value", + "Get register value as a list of strings.", + ); + template_function_arity_1( + "set-editor-clip-top!", + "Set the editor clipping at the top.", + ); + template_function_arity_1( + "set-editor-clip-right!", + "Set the editor clipping at the right.", + ); + template_function_arity_1( + "set-editor-clip-left!", + "Set the editor clipping at the left.", + ); + template_function_arity_1( + "set-editor-clip-bottom!", + "Set the editor clipping at the 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!"); + template_function_arity_2("set-register!"); + + if let Some(mut target_directory) = alternative_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) + }); + } + + target_directory.push("editor.scm"); + + std::fs::write(target_directory, &builtin_editor_command_module).unwrap(); + } + + engine.register_steel_module( + "helix/editor.scm".to_string(), + builtin_editor_command_module, + ); + } + + // 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) { + std::thread::spawn(initialize_engine); + } + + fn reinitialize(&self) { + reload_engine(); + } + + fn engine_name(&self) -> super::PluginSystemKind { + super::PluginSystemKind::Steel + } + + fn run_initialization_script( + &self, + cx: &mut Context, + configuration: Arc>>, + language_configuration: Arc>, + event_reader: TerminalEventReaderHandle, + ) { + run_initialization_script(cx, configuration, language_configuration, event_reader); + } + + fn handle_keymap_event( + &self, + editor: &mut ui::EditorView, + mode: Mode, + cxt: &mut Context, + event: KeyEvent, + ) -> Option { + SteelScriptingEngine::handle_keymap_event_impl(self, editor, mode, cxt, event) + } + + fn call_function_by_name(&self, cx: &mut Context, name: &str, args: &[Cow]) -> bool { + if enter_engine(|x| x.global_exists(name)) { + let mut 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); + engine + .call_function_by_name_with_args_from_mut_slice(name, &mut args) + }, + ) + }) + } + }) { + 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 + let res = 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) => { + match &res { + SteelVal::Void => {} + SteelVal::StringV(s) => { + ctx.editor.set_status(s.as_str().to_owned()); + } + _ => { + ctx.editor.set_status(res.to_string()); + } + } + + Ok(res) + } + Err(e) => Err(e), + }; + + patch_callbacks(&mut ctx); + + res + }; + + res + }) { + let mut ctx = Context { + register: None, + count: None, + editor: cx.editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs: 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 { + try_enter_engine(|engine| get_doc_for_global(engine, ident)).unwrap_or_default() + } + + // Just dump docs for all top level values? + fn available_commands<'a>(&self) -> Vec> { + try_enter_engine(|engine| { + engine + .readable_globals(GLOBAL_OFFSET.load(std::sync::atomic::Ordering::Relaxed)) + .iter() + .map(|x| x.resolve().to_string().into()) + .collect() + }) + .unwrap_or_default() + } + + fn generate_sources(&self) { + fn format_markdown_doc(writer: &mut W, doc: &str) { + for line in doc.lines() { + if line.starts_with("# ") { + write!(writer, "###").unwrap(); + } + writeln!(writer, "{}", line).unwrap(); + } + } + + // Generate sources directly with a fresh engine + let mut engine = Engine::new(); + configure_builtin_sources(&mut engine, true); + // Generate documentation as well + if let Some(target) = alternative_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(); + + // Also generate docs for the built in modules + let module = engine.builtin_modules().get("helix/core/text").unwrap(); + + writeln!(&mut writer, "# helix/core/text").unwrap(); + writeln!( + &mut writer, + "To use, you can include with `(require-builtin helix/core/text)`" + ) + .unwrap(); + + let mut found_definitions = std::collections::HashSet::new(); + + let mut exported_functions: Vec<_> = module + .names() + .into_iter() + .filter(|name| !name.starts_with("#%")) + .collect(); + + exported_functions.sort(); + + for name in &exported_functions { + if let Some(value) = module.documentation().get(name) { + found_definitions.insert(name.to_string()); + + if let steel::steel_vm::builtin::Documentation::Markdown(m) = value { + let escaped = name.replace("*", "\\*"); + writeln!(&mut writer, "### **{}**", escaped).unwrap(); + + format_markdown_doc(&mut writer, &m.0); + } + } + } + + for name in exported_functions { + if !found_definitions.contains(&name) { + writeln!(&mut writer, "### **{}**", name).unwrap(); + } + } + } + } + + // TODO: Should this just be a hook / event instead of a function like this? + // Handle an LSP notification, assuming its been sent through + fn handle_lsp_call( + &self, + cx: &mut compositor::Context, + server_id: helix_lsp::LanguageServerId, + event_name: String, + call_id: jsonrpc::Id, + params: helix_lsp::jsonrpc::Params, + ) -> Option> { + let mut ctx = Context { + register: None, + count: None, + editor: cx.editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs: cx.jobs, + }; + + let language_server_name = ctx + .editor + .language_servers + .get_by_id(server_id) + .map(|x| x.name().to_owned()); + + let Some(language_server_name) = language_server_name else { + ctx.editor.set_error("Unable to find language server"); + return None; + }; + + let mut pass_call_id = false; + + let id = LspCallRegistryId { + lsp_name: language_server_name, + event_name, + generation: load_generation(), + }; + + let function = LSP_CALL_REGISTRY + .read() + .unwrap() + .map + .get(&id) + .map(|x| match x { + LspKind::Call(rooted_steel_val) => { + pass_call_id = true; + rooted_steel_val.value() + } + LspKind::Notification(rooted_steel_val) => rooted_steel_val.value(), + }) + .cloned(); + + let result = if let Some(function) = function { + enter_engine(|guard| { + // Install the interrupt handler, in the event this thing + // is blocking for too long. + with_interrupt_handler(|| { + guard + .with_mut_reference::(&mut ctx) + .consume(move |engine, arguments| { + let context = arguments[0].clone(); + engine.update_value("*helix.cx*", context); + + let params = serde_json::to_value(¶ms) + .map_err(|e| SteelErr::new(ErrorKind::Generic, e.to_string())) + .and_then(|x| x.into_steelval())?; + + if pass_call_id { + let call_id = serde_json::to_value(&call_id) + .map_err(|e| SteelErr::new(ErrorKind::Generic, e.to_string())) + .and_then(|x| x.into_steelval())?; + + let mut arguments = [call_id, params]; + + engine.call_function_with_args_from_mut_slice( + function.clone(), + &mut arguments, + ) + } else { + let mut arguments = [params]; + engine.call_function_with_args_from_mut_slice( + function.clone(), + &mut arguments, + ) + } + }) + }) + }) + } else { + Ok(SteelVal::Void) + }; + + patch_callbacks(&mut ctx); + + let value = match result { + Err(e) => { + cx.editor.set_error(format!("{}", e)); + Some(SteelVal::Void) + } + Ok(value) => Some(value), + }?; + + match value { + SteelVal::Void => None, + value => { + let serde_value: Result = value.try_into(); + match serde_value { + Ok(serialized_value) => Some(Ok(serialized_value)), + Err(error) => { + log::warn!("Failed to serialize a SteelVal: {}", error); + None + } + } + } + } + } +} + +fn patch_callbacks(ctx: &mut Context<'_>) { + for callback in std::mem::take(&mut ctx.callback) { + let callback = async move { + let call: Box = Box::new( + move |editor: &mut Editor, compositor: &mut Compositor, jobs| { + callback( + compositor, + &mut compositor::Context { + editor, + scroll: None, + jobs, + }, + ) + }, + ); + Ok(call) + }; + + ctx.jobs.local_callback(callback); + } +} + +impl SteelScriptingEngine { + fn handle_keymap_event_impl( + &self, + editor: &mut ui::EditorView, + mode: Mode, + cx: &mut Context, + event: KeyEvent, + ) -> Option { + 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); + &view.doc + }; + + if let Some(extension) = extension { + let map = get_extension_keymap(); + let keymap = map.get_extension(extension); + + if let Some(keymap) = keymap { + let res = editor.keymaps.get_with_map(&keymap.0, mode, event); + + if let KeymapResult::NotFound = res { + return None; + } + + return Some(res); + } + } + + let map = get_extension_keymap(); + + if let Some(keymap) = map.get_doc_id(document_id_to_usize(doc_id)) { + let res = editor.keymaps.get_with_map(&keymap.0, mode, event); + + if let KeymapResult::NotFound = res { + return None; + } + + return Some(res); + } + + 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); +} + +pub fn present_error_inside_engine_context_with_callback( + cx: &mut Context, + engine: &mut Engine, + e: SteelErr, + mut callback: impl FnMut(&mut Compositor) + Send + Sync + 'static, +) { + 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); + + callback(compositor); + } + }, + )); + 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) -> anyhow::Result { + Ok(EmbeddedKeyMap(serde_json::from_str(&value)?)) +} + +pub fn merge_keybindings(left: &mut EmbeddedKeyMap, right: EmbeddedKeyMap) { + merge_keys(&mut left.0, right.0) +} + +pub fn query_keybindings( + map: &mut EmbeddedKeyMap, + mode: SteelString, + keybindings: Vec, +) -> anyhow::Result { + let mode = match mode.as_str() { + "normal" => Mode::Normal, + "select" => Mode::Select, + "insert" => Mode::Insert, + _ => anyhow::bail!("unknown mode: {}", mode), + }; + + let keymap = map.0.get(&mode); + let bindings = keybindings + .into_iter() + .map(|x| KeyEvent::from_str(&x)) + .collect::>>()?; + + if let Some(keymap) = keymap { + let value = keymap.search(&bindings); + match value { + Some(KeyTrie::MappableCommand(k)) => Ok(SteelVal::StringV(k.name().into())), + _ => Ok(SteelVal::BoolV(false)), + } + } else { + Ok(SteelVal::BoolV(false)) + } +} + +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 let Ok(steel_config_dir) = std::env::var("HELIX_STEEL_CONFIG") { + PathBuf::from(steel_config_dir).join(file_name) + } else 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 { + // Lets go ahead and just deserialize it that way. + // It's ugly and annoying. + config: LanguageConfiguration, +} + +// TODO: @Matt 5/19/2025 - Finish up writing these bindings. +impl Custom for IndividualLanguageConfiguration {} + +impl Custom for HelixConfiguration {} + +fn update_configuration_for_all_open_documents(ctx: &mut Context) { + for document in ctx.editor.documents.values_mut() { + if let Some(name) = document.language_name() { + let config_for_file = ctx + .editor + .syn_loader + .load() + .language_configs() + .find(|x| x.language_id == name) + .cloned() + .map(Arc::new); + 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)); + } +} + +fn filter_null_values(value: &mut Value) { + match value { + Value::Object(map) => { + map.retain(|_, v| { + if v.is_null() { + false + } else { + filter_null_values(v); + true + } + }); + } + Value::Array(arr) => { + arr.retain_mut(|v| { + if v.is_null() { + false + } else { + filter_null_values(v); + true + } + }); + } + Value::Number(n) => { + if let Some(f) = n.as_f64() { + *n = (f.round() as i64).into(); + } + } + _ => {} + } +} + +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_configs() + .find(|x| x.language_id == language.as_str()) + .and_then(|x| { + let config = serde_json::json!(x); + + SteelVal::try_from(config).ok() + }) + } + + fn update_language_config( + &mut self, + language: SteelString, + config: SteelVal, + ) -> anyhow::Result<()> { + // Do some gross json -> toml conversion + let mut value = serde_json::Value::try_from(config)?; + + filter_null_values(&mut value); + + // Horrendous, disgusting + let mut toml_value: toml::Value = serde_json::from_str(&serde_json::to_string(&value)?)?; + + let auto_format_present = toml_value.get("auto-format").is_some(); + let diagnostic_severity_present = toml_value.get("diagnostic-severity").is_some(); + let language_servers_present = toml_value.get("language-servers").is_some(); + let persistent_diagnostic_sources_present = + toml_value.get("persistent-diagnostic-sources").is_some(); + + // Existing language config: + let mut existing_config = self + .language_configuration + .load() + .language_configs() + .find(|x| x.language_id == language.as_str()) + .unwrap() + .clone(); + + if toml_value.get("scope").is_none() { + toml_value + .as_table_mut() + .and_then(|x| x.insert("scope".to_string(), existing_config.scope.into())); + } + + for need_empty in ["file-types", "shebangs", "roots"] { + if toml_value.get(need_empty).is_none() { + toml_value.as_table_mut().and_then(|x| { + x.insert(need_empty.to_owned(), >::new().into()) + }); + } + } + + let new_config: LanguageConfiguration = toml_value.try_into()?; + + if let Some(id) = new_config.language_server_language_id { + existing_config.language_server_language_id = Some(id); + } + + // Take the new scope, since its already set to the old one as a default. + existing_config.scope = new_config.scope; + + if !new_config.file_types.is_empty() { + existing_config.file_types = new_config.file_types; + } + + if !new_config.shebangs.is_empty() { + existing_config.shebangs = new_config.shebangs; + } + + if !new_config.roots.is_empty() { + existing_config.roots = new_config.roots; + } + + if let Some(comment_tokens) = new_config.comment_tokens { + existing_config.comment_tokens = Some(comment_tokens); + } + + if let Some(block_comment_tokens) = new_config.block_comment_tokens { + existing_config.block_comment_tokens = Some(block_comment_tokens); + } + + if let Some(text_width) = new_config.text_width { + existing_config.text_width = Some(text_width); + } + + if let Some(soft_wrap) = new_config.soft_wrap { + existing_config.soft_wrap = Some(soft_wrap); + } + + if auto_format_present { + existing_config.auto_format = new_config.auto_format; + } + + if let Some(formatter) = new_config.formatter { + existing_config.formatter = Some(formatter); + } + + if let Some(path_complation) = new_config.path_completion { + existing_config.path_completion = Some(path_complation); + } + + if diagnostic_severity_present { + existing_config.diagnostic_severity = new_config.diagnostic_severity; + } + + if let Some(grammar) = new_config.grammar { + existing_config.grammar = Some(grammar); + } + + if let Some(injection_regex) = new_config.injection_regex { + existing_config.injection_regex = Some(injection_regex); + } + + if language_servers_present { + existing_config.language_servers = new_config.language_servers; + } + + if let Some(indent) = new_config.indent { + existing_config.indent = Some(indent); + } + + if let Some(debugger) = new_config.debugger { + existing_config.debugger = Some(debugger); + } + + if let Some(auto_pairs) = new_config.auto_pairs { + existing_config.auto_pairs = Some(auto_pairs); + } + + if let Some(rulers) = new_config.rulers { + existing_config.rulers = Some(rulers); + } + + if let Some(workspace_lsp_roots) = new_config.workspace_lsp_roots { + existing_config.workspace_lsp_roots = Some(workspace_lsp_roots); + } + + if let Some(rainbow) = new_config.rainbow_brackets { + existing_config.rainbow_brackets = Some(rainbow); + } + + if persistent_diagnostic_sources_present { + existing_config.persistent_diagnostic_sources = + new_config.persistent_diagnostic_sources; + } + + self.update_individual_language_config(IndividualLanguageConfiguration { + config: existing_config, + }); + + Ok(()) + } + + fn get_language_server_config(&self, lsp: SteelString) -> Option { + let loader = (*(*self.language_configuration.load())).clone(); + let lsp_configs = loader.language_server_configs(); + let individual_config = lsp_configs.get(lsp.as_str())?; + let mut json = serde_json::json!(individual_config); + + if let Some(config) = individual_config.config.clone() { + json["config"] = config; + } + + let hash = SteelVal::try_from(json); + + hash.ok() + } + + fn update_language_server_config( + &mut self, + lsp: SteelString, + map: HashMap, + ) -> anyhow::Result<()> { + let mut loader = (*(*self.language_configuration.load())).clone(); + let lsp_configs = loader.language_server_configs_mut(); + + let individual_config = lsp_configs.get_mut(lsp.as_str()); + + if let Some(config) = individual_config { + if let Some(args) = map.get("args") { + config.args = >::from_steelval(args)?; + } + + if let Some(command) = map.get("command") { + config.command = String::from_steelval(command)?; + } + + if let Some(environment) = map.get("environment") { + config.environment = >::from_steelval(environment)?; + } + + if let Some(config_json) = map.get("config") { + let mut serialized = serde_json::Value::try_from(config_json.clone())?; + + filter_null_values(&mut serialized); + + config.config = Some(serialized); + } + + if let Some(timeout) = map.get("timeout") { + config.timeout = match timeout { + SteelVal::IntV(i) => *i as u64, + SteelVal::NumV(n) if n.fract() == 0.0 => n.round() as u64, + _ => anyhow::bail!("Unable to convert timeout to integer, found: {}", timeout), + }; + } + + if let Some(required_root_patterns) = map.get("required-root-patterns") { + let patterns = >::from_steelval(required_root_patterns)?; + + if !patterns.is_empty() { + let mut builder = globset::GlobSetBuilder::new(); + for pattern in patterns { + let glob = globset::Glob::new(&pattern)?; + builder.add(glob); + } + config.required_root_patterns = Some(builder.build()?); + } + } + } else { + let command = if let Some(command) = map.get("command") { + String::from_steelval(command)? + } else { + anyhow::bail!("LSP config missing required `command` field."); + }; + + let mut config = LanguageServerConfiguration { + command, + args: Vec::new(), + environment: HashMap::new(), + config: None, + timeout: default_timeout(), + required_root_patterns: None, + }; + + if let Some(args) = map.get("args") { + config.args = >::from_steelval(args)?; + } + + if let Some(environment) = map.get("environment") { + config.environment = >::from_steelval(environment)?; + } + + if let Some(config_json) = map.get("config") { + let serialized = serde_json::Value::try_from(config_json.clone())?; + config.config = Some(serialized); + } + + if let Some(timeout) = map.get("timeout") { + config.timeout = u64::from_steelval(timeout)?; + } + + if let Some(required_root_patterns) = map.get("required-root-patterns") { + let patterns = >::from_steelval(required_root_patterns)?; + + if !patterns.is_empty() { + let mut builder = globset::GlobSetBuilder::new(); + for pattern in patterns { + let glob = globset::Glob::new(&pattern)?; + builder.add(glob); + } + config.required_root_patterns = Some(builder.build()?); + } + } + + lsp_configs.insert(lsp.as_str().to_owned(), config); + } + + self.language_configuration.store(Arc::new(loader)); + + Ok(()) + } + + // 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; + } + } + + self.language_configuration.store(Arc::new(loader)); + } + + fn load_config(&self) -> Config { + (*self.configuration.load_full().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 set_keybindings(&self, keybindings: EmbeddedKeyMap) { + let mut app_config = self.load_config(); + app_config.keys = keybindings.0; + self.store_config(app_config); + } + + fn get_keybindings(&self) -> EmbeddedKeyMap { + EmbeddedKeyMap(self.configuration.load_full().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); + } + + fn jump_label_alphabet(&self, alphabet: String) { + let mut app_config = self.load_config(); + app_config.editor.jump_label_alphabet = alphabet.chars().collect(); + self.store_config(app_config); + } + + fn line_number(&self, mode_config: SteelVal) -> anyhow::Result<()> { + let config = match mode_config { + SteelVal::StringV(s) | SteelVal::SymbolV(s) => match s.as_str() { + "relative" => LineNumber::Relative, + "absolute" => LineNumber::Absolute, + other => anyhow::bail!("Unrecognized line-number option: {}", other), + }, + other => anyhow::bail!("Unrecognized line-number option: {}", other), + }; + + let mut app_config = self.load_config(); + app_config.editor.line_number = config; + self.store_config(app_config); + Ok(()) + } + + 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); + } + + 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_other_lines_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.other_lines = DiagnosticFilter::Enable(severity); + self.store_config(app_config); + } + + fn inline_diagnostics_min_diagnostic_width(&self, min_diagnostic_width: u16) { + let mut app_config = self.load_config(); + app_config.editor.inline_diagnostics.min_diagnostic_width = min_diagnostic_width; + self.store_config(app_config); + } + + fn inline_diagnostics_prefix_len(&self, prefix_len: u16) { + let mut app_config = self.load_config(); + app_config.editor.inline_diagnostics.prefix_len = prefix_len; + self.store_config(app_config); + } + + fn inline_diagnostics_max_wrap(&self, max_wrap: u16) { + let mut app_config = self.load_config(); + app_config.editor.inline_diagnostics.max_wrap = max_wrap; + self.store_config(app_config); + } + + fn inline_diagnostics_max_diagnostics(&self, max_diagnostics: usize) { + let mut app_config = self.load_config(); + app_config.editor.inline_diagnostics.max_diagnostics = max_diagnostics; + 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: HashMap) -> anyhow::Result<()> { + let mut app_config = self.load_config(); + + fn steel_to_elements(val: &SteelVal) -> anyhow::Result { + if let SteelVal::StringV(s) = val { + let value = match s.as_str() { + "mode" => StatusLineElement::Mode, + "spinner" => StatusLineElement::Spinner, + "file-base-name" => StatusLineElement::FileBaseName, + "file-name" => StatusLineElement::FileName, + "file-absolute-path" => StatusLineElement::FileAbsolutePath, + "file-modification-indicator" => StatusLineElement::FileModificationIndicator, + "read-only-indicator" => StatusLineElement::ReadOnlyIndicator, + "file-encoding" => StatusLineElement::FileEncoding, + "file-line-ending" => StatusLineElement::FileLineEnding, + "file-indent-style" => StatusLineElement::FileIndentStyle, + "file-type" => StatusLineElement::FileType, + "diagnostics" => StatusLineElement::Diagnostics, + "workspace-diagnostics" => StatusLineElement::WorkspaceDiagnostics, + "selections" => StatusLineElement::Selections, + "primary-selection-length" => StatusLineElement::PrimarySelectionLength, + "position" => StatusLineElement::Position, + "separator" => StatusLineElement::Separator, + "position-percentage" => StatusLineElement::PositionPercentage, + "total-line-numbers" => StatusLineElement::TotalLineNumbers, + "spacer" => StatusLineElement::Spacer, + "version-control" => StatusLineElement::VersionControl, + "register" => StatusLineElement::Register, + "current-working-directory" => StatusLineElement::CurrentWorkingDirectory, + _ => anyhow::bail!("Unknown status line element: {}", s), + }; + + return Ok(value); + } else { + anyhow::bail!("Cannot convert value to status line element: {}", val) + } + } + + fn steel_list_to_elements(val: &SteelVal) -> anyhow::Result> { + if let SteelVal::ListV(l) = val { + return l.iter().map(steel_to_elements).collect(); + } else { + anyhow::bail!( + "Cannot convert value to vec of status line element: {}", + val + ) + } + } + + fn steel_to_severity(val: &SteelVal) -> anyhow::Result { + if let SteelVal::StringV(s) = val { + let value = match s.as_str() { + "hint" => Severity::Hint, + "info" => Severity::Info, + "warning" => Severity::Warning, + "error" => Severity::Error, + _ => anyhow::bail!("Unknown severity label: {}", s), + }; + + return Ok(value); + } else { + anyhow::bail!("Cannot convert value to severity: {}", val) + } + } + + fn steel_list_to_severity_vec(val: &SteelVal) -> anyhow::Result> { + if let SteelVal::ListV(l) = val { + return l.iter().map(steel_to_severity).collect(); + } else { + anyhow::bail!( + "Cannot convert value to vec of status line element: {}", + val + ) + } + } + + if let Some(left) = config.get("left") { + app_config.editor.statusline.left = steel_list_to_elements(left)?; + } + + if let Some(center) = config.get("center") { + app_config.editor.statusline.center = steel_list_to_elements(center)?; + } + + if let Some(right) = config.get("right") { + app_config.editor.statusline.right = steel_list_to_elements(right)?; + } + + if let Some(separator) = config.get("separator") { + app_config.editor.statusline.separator = String::from_steelval(separator)?; + } + + if let Some(normal_mode) = config.get("mode-normal") { + if let SteelVal::StringV(s) = normal_mode { + app_config.editor.statusline.mode.normal = s.as_str().to_owned(); + } else { + anyhow::bail!("mode normal expects a string, found: {}", normal_mode); + } + } + + if let Some(insert_mode) = config.get("mode-insert") { + if let SteelVal::StringV(s) = insert_mode { + app_config.editor.statusline.mode.insert = s.as_str().to_owned(); + } else { + anyhow::bail!("mode insert expects a string, found: {}", insert_mode); + } + } + + if let Some(select_mode) = config.get("mode-select") { + if let SteelVal::StringV(s) = select_mode { + app_config.editor.statusline.mode.select = s.as_str().to_owned(); + } else { + anyhow::bail!("mode normal expects a string, found: {}", select_mode); + } + } + + if let Some(diagnostics) = config.get("diagnostics") { + app_config.editor.statusline.diagnostics = steel_list_to_severity_vec(diagnostics)?; + } + + if let Some(diagnostics) = config.get("workspace-diagnostics") { + app_config.editor.statusline.workspace_diagnostics = + steel_list_to_severity_vec(diagnostics)?; + } + + self.store_config(app_config); + + Ok(()) + } + + 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, smart_case: bool, wrap_around: bool) { + let mut app_config = self.load_config(); + app_config.editor.search = SearchConfig { + smart_case, + wrap_around, + }; + self.store_config(app_config); + } + + fn lsp(&self, config: HashMap) -> anyhow::Result<()> { + let mut app_config = self.load_config(); + + if let Some(enabled) = config.get("enable") { + app_config.editor.lsp.enable = bool::from_steelval(enabled)?; + } + + if let Some(display) = config.get("display-progress-messages") { + app_config.editor.lsp.display_progress_messages = bool::from_steelval(display)?; + } + + if let Some(display) = config.get("display-messages") { + app_config.editor.lsp.display_messages = bool::from_steelval(display)?; + } + + if let Some(auto) = config.get("auto-signature-help") { + app_config.editor.lsp.auto_signature_help = bool::from_steelval(auto)?; + } + + if let Some(display) = config.get("display-signature-help") { + app_config.editor.lsp.display_signature_help_docs = bool::from_steelval(display)?; + } + + if let Some(display) = config.get("display-inlay-hints") { + app_config.editor.lsp.display_inlay_hints = bool::from_steelval(display)?; + } + + if let Some(limit) = config.get("inlay-hints-length-limit") { + let n = NonZeroU8::new(u8::from_steelval(limit)?); + + if let Some(n) = n { + app_config.editor.lsp.inlay_hints_length_limit = Some(n) + } else { + anyhow::bail!("inlay hints length limit provided was zero") + } + } + + if let Some(display) = config.get("display-color-swatches") { + app_config.editor.lsp.display_color_swatches = bool::from_steelval(display)?; + } + + if let Some(snippets) = config.get("snippets") { + app_config.editor.lsp.snippets = bool::from_steelval(snippets)?; + } + + if let Some(goto) = config.get("goto-reference-include-declaration") { + app_config.editor.lsp.goto_reference_include_declaration = bool::from_steelval(goto)?; + } + + self.store_config(app_config); + Ok(()) + } + + 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, buffer_config: SteelVal) -> anyhow::Result<()> { + let config = match buffer_config { + SteelVal::StringV(s) | SteelVal::SymbolV(s) => match s.as_str() { + "never" => BufferLine::Never, + "always" => BufferLine::Always, + "multiple" => BufferLine::Multiple, + other => anyhow::bail!("Unrecognized bufferline option: {}", other), + }, + other => anyhow::bail!("Unrecognized bufferline option: {}", other), + }; + + let mut app_config = self.load_config(); + app_config.editor.bufferline = config; + self.store_config(app_config); + + Ok(()) + } + + 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); + } + + fn rainbow_brackets(&self, config: bool) { + let mut app_config = self.load_config(); + app_config.editor.rainbow_brackets = 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 readable_globals = engine.readable_globals(GLOBAL_OFFSET.load(Ordering::Relaxed)); + + for global in readable_globals { + if global.resolve() == ident { + return engine.get_doc_for_identifier(ident); + } + } + + None + } 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>, + event_reader: TerminalEventReaderHandle, +) { + let now = std::time::Instant::now(); + install_event_reader(event_reader); + + // Hack: + // This might be fussed with, and under re initialization we want + // to reset this back to what it was before. + cx.editor.editor_clipping = ClippingConfiguration::default(); + + log::info!("Loading init.scm..."); + + let helix_module_path = helix_module_file(); + let helix_init_path = steel_init_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(), + ); + + if helix_module_path.exists() { + log::info!("Loading helix.scm from context: {:?}", helix_init_path); + let res = guard.run_with_reference_from_path( + cx, + "*helix.cx*", + &format!(r#"(require {:?})"#, helix_module_path.to_str().unwrap()), + helix_init_path, + ); + + // Present the error in the helix.scm loading + if let Err(e) = res { + present_error_inside_engine_context(cx, guard, e); + return; + } + } else { + println!("Unable to find the `helix.scm` file, creating...."); + std::fs::write(helix_module_path, "").ok(); + } + + 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."); + std::fs::write(helix_module_path, "").ok(); + } + }); + + patch_callbacks(cx); + + log::info!("Steel init time: {:?}", now.elapsed()); +} + +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(); + let generation = load_generation(); + + match event_kind.as_str() { + "on-mode-switch" => { + register_hook!(move |event: &mut OnModeSwitch<'_, '_>| { + if let Err(e) = enter_engine(|guard| { + if !is_current_generation(generation) { + return Ok(SteelVal::Void); + } + + 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_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| { + if !is_current_generation(generation) { + return Ok(SteelVal::Void); + } + + 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| { + if !is_current_generation(generation) { + return Ok(SteelVal::Void); + } + + 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, + }; + let res = enter_engine(|guard| { + if !is_current_generation(generation) { + return; + } + + 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); + } + }); + + patch_callbacks(&mut ctx); + + res + }; + 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, + }; + let res = enter_engine(|guard| { + if !is_current_generation(generation) { + return; + } + + 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); + } + }); + + patch_callbacks(&mut ctx); + + res + }; + 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, + }; + let res = enter_engine(|guard| { + if !is_current_generation(generation) { + return; + } + + 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); + } + }); + + patch_callbacks(&mut ctx); + + res + }; + job::dispatch_blocking_jobs(callback); + + Ok(()) + }); + + Ok(SteelVal::Void).into() + } + + "document-saved" => { + // TODO: Share this code with the above since most of it is + // exactly the same + register_hook!(move |event: &mut DocumentSaved<'_>| { + 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, + }; + let res = enter_engine(|guard| { + if !is_current_generation(generation) { + return; + } + + 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); + } + }); + + patch_callbacks(&mut ctx); + + res + }; + 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() { + use std::fmt::Write; + let mut path = steel_lsp_home_dir(); + 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", + "find-workspace", + "doc-id->usize", + "new-component!", + "acquire-context-lock", + "SteelDynamicComponent?", + "prompt", + "picker", + "#%exp-picker", + "Component::Text", + "hx.create-directory", + ]; + + for value in names { + writeln!(&mut output, "(#%register-global '{})", value).unwrap(); + } + + writeln!(&mut output).unwrap(); + let search_path = helix_loader::config_dir(); + let search_path_str = search_path.to_str().unwrap(); + + #[cfg(target_os = "windows")] + let search_path_str: String = search_path_str.escape_default().collect(); + + writeln!( + &mut output, + "(#%register-additional-search-path \"{}\")", + search_path_str + ) + .unwrap(); + + for dir in helix_loader::runtime_dirs() { + let dir = dir.to_str().unwrap(); + + #[cfg(target_os = "windows")] + let dir: String = dir.escape_default().collect(); + + writeln!( + &mut output, + "(#%register-additional-search-path \"{}\")", + dir + ) + .unwrap(); + } + + std::fs::write(path, output).unwrap(); +} + +fn configure_lsp_builtins(name: &str, module: &BuiltInModule) { + use std::fmt::Write; + let mut path = steel_lsp_home_dir(); + 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() { + writeln!(&mut output, "{}", value).unwrap(); + } + + output.push_str("))"); + + for value in module.names() { + if let Some(doc) = module.get_documentation(&value) { + output.push_str(&format!( + "(#%module-add-doc #%helix-{}-module {:?} {:?})\n", + name, value, doc + )); + } + } + + 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); +} + +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, doc: &str| { + if generate_sources { + let doc = format_docstring(doc); + builtin_misc_module.push_str(&format!( + r#" +(provide {}) +;;@doc +{} +(define ({}) + (helix.{} *helix.cx*)) +"#, + name, doc, name, name + )); + } + }; + + // Arity 0 + module.register_fn("hx.cx->pos", cx_pos_within_text); + module.register_fn("cursor-position", 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", "DEPRECATED: Please use `cursor-position`"); + template_function_arity_0( + "cursor-position", + "Returns the cursor position within the current buffer as an integer", + ); + + module.register_fn("get-active-lsp-clients", get_active_lsp_clients); + template_function_arity_0( + "get-active-lsp-clients", + "Get all language servers, that are attached to the current buffer", + ); + + let mut template_function_no_context = |name: &str, doc: &str| { + if generate_sources { + let docstring = format_docstring(doc); + + builtin_misc_module.push_str(&format!( + r#" +(provide {}) +;;@doc +{} +(define {} helix.{}) + "#, + name, docstring, name, name + )) + } + }; + + template_function_no_context( + "mode-switch-old", + "Return the old mode from the event payload", + ); + template_function_no_context( + "mode-switch-new", + "Return the new mode from the event payload", + ); + + module.register_fn("lsp-client-initialized?", is_lsp_client_initialized); + template_function_no_context( + "lsp-client-initialized?", + "Return if the lsp client is initialized", + ); + module.register_fn("lsp-client-name", lsp_client_name); + template_function_no_context("lsp-client-name", "Get the name of the lsp client"); + module.register_fn("lsp-client-offset-encoding", lsp_client_offset_encoding); + template_function_no_context( + "lsp-client-offset-encoding", + "Get the offset encoding of the lsp client", + ); + + let mut template_function_arity_1 = |name: &str, doc: &str| { + let doc = format_docstring(doc); + if generate_sources { + builtin_misc_module.push_str(&format!( + r#" +(provide {}) +;;@doc +{} +(define ({} arg) + (helix.{} *helix.cx* arg)) +"#, + name, doc, name, name + )); + } + }; + + macro_rules! register_1 { + ($name:expr, $func:expr, $doc:expr) => {{ + module.register_fn($name, $func); + template_function_arity_1($name, $doc); + }}; + } + + // TODO: Get rid of the `hx.` prefix + register_1!( + "hx.custom-insert-newline", + custom_insert_newline, + "DEPRECATED: Please use `insert-newline-hook`" + ); + register_1!( + "insert-newline-hook", + custom_insert_newline, + r#"Inserts a new line with the provided indentation. + +```scheme +(insert-newline-hook indent-string) +``` + +indent-string : string? + +"# + ); + register_1!( + "push-component!", + push_component, + r#" +Push a component on to the top of the stack. + +```scheme +(push-component! component) +``` + +component : WrappedDynComponent? + "# + ); + + // Arity 1 + register_1!( + "pop-last-component!", + pop_last_component_by_name, + "DEPRECATED: Please use `pop-last-component-by-name!`" + ); + register_1!( + "pop-last-component-by-name!", + pop_last_component_by_name, + r#"Pops the last component off of the stack by name. In other words, +it removes the component matching this name from the stack. + +```scheme +(pop-last-component-by-name! name) +``` + +name : string? + "# + ); + + register_1!( + "on-key-callback", + enqueue_on_next_key, + r#" +Enqueue a function to be run on the next keypress. The function must accept +a key event as an argument. This currently will only will work if the command is +called via a keybinding. + "# + ); + + register_1!( + "trigger-on-key-callback", + trigger_callback, + r#" +Trigger an on key callback if it exists with the specified key event. + "# + ); + + register_1!( + "enqueue-thread-local-callback", + enqueue_command, + r#" +Enqueue a function to be run following this context of execution. This could +be useful for yielding back to the editor in the event you want updates to happen +before your function is run. + +```scheme +(enqueue-thread-local-callback callback) +``` + +callback : (-> any?) + Function with no arguments. + +# Examples + +```scheme +(enqueue-thread-local-callback (lambda () (theme "focus_nova"))) +``` + "# + ); + + register_1!( + "set-status!", + set_status, + "Sets the content of the status line, with the info severity" + ); + + register_1!( + "set-warning!", + set_warning, + "Sets the content of the status line, with the warning severity" + ); + + register_1!( + "set-error!", + set_error, + "Sets the content of the status line, with the error severity" + ); + + module.register_fn("send-lsp-command", send_arbitrary_lsp_command); + module.register_fn("send-lsp-notification", send_arbitrary_lsp_notification); + 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)) + "#, + ); + } + + if generate_sources { + builtin_misc_module.push_str( + r#" + (provide send-lsp-notification) + ;;@doc + ;; Send an LSP notification. 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. Unlike + ;; `send-lsp-command`, this does not expect a response and is used for + ;; fire-and-forget notifications. + ;; + ;; # Example + ;; ```scheme + ;; (send-lsp-notification "copilot" + ;; "textDocument/didShowCompletion" + ;; (hash "item" + ;; (hash "insertText" "a helpful suggestion" + ;; "range" (hash "start" (hash "line" 1 "character" 0) + ;; "end" (hash "line" 1 "character" 2))))) + ;; ``` + (define (send-lsp-notification lsp-name method-name params) + (helix.send-lsp-notification *helix.cx* lsp-name method-name params)) + "#, + ); + } + + module.register_fn("lsp-reply-ok", lsp_reply_ok); + if generate_sources { + builtin_misc_module.push_str( + r#" + (provide lsp-reply-ok) + ;;@doc + ;; Send a successful reply to an LSP request with the given result. + ;; + ;; ```scheme + ;; (lsp-reply-ok lsp-name request-id result) + ;; ``` + ;; + ;; * lsp-name : string? - Name of the language server + ;; * request-id : string? - ID of the request to respond to + ;; * result : any? - The result value to send back + ;; + ;; # Examples + ;; ```scheme + ;; ;; Reply to a request with id "123" from rust-analyzer + ;; (lsp-reply-ok "rust-analyzer" "123" (hash "result" "value")) + ;; ``` + (define (lsp-reply-ok lsp-name request-id result) + (helix.lsp-reply-ok *helix.cx* lsp-name request-id result)) + "#, + ); + } + + macro_rules! register_2_no_context { + ($name:expr, $func:expr, $doc:expr) => {{ + module.register_fn($name, $func); + if generate_sources { + let doc = format_docstring($doc); + builtin_misc_module.push_str(&format!( + r#" +(provide {}) +;;@doc +{} +(define ({} arg1 arg2) + (helix.{} arg1 arg2)) +"#, + $name, doc, $name, $name + )); + } + }}; + } + + register_2_no_context!( + "acquire-context-lock", + acquire_context_lock, + r#" +Schedule a function to run on the main thread. This is a fairly low level function, and odds are +you'll want to use some abstractions on top of this. + +The provided function will get enqueued to run on the main thread, and during the duration of the functions +execution, the provided mutex will be locked. + +```scheme +(acquire-context-lock callback-fn mutex) +``` + +callback-fn : (-> void?) + Function with no arguments + +mutex : mutex? +"# + ); + + let mut template_function_arity_2 = |name: &str, doc: &str| { + if generate_sources { + let doc = format_docstring(doc); + builtin_misc_module.push_str(&format!( + r#" +(provide {}) +;;@doc +{} +(define ({} arg1 arg2) + (helix.{} *helix.cx* arg1 arg2)) +"#, + name, doc, name, name + )); + } + }; + + macro_rules! register_2 { + ($name:expr, $func:expr, $doc:expr) => {{ + module.register_fn($name, $func); + template_function_arity_2($name, $doc); + }}; + } + + // Arity 2 + register_2!( + "enqueue-thread-local-callback-with-delay", + enqueue_command_with_delay, + r#" +Enqueue a function to be run following this context of execution, after a delay. This could +be useful for yielding back to the editor in the event you want updates to happen +before your function is run. + +```scheme +(enqueue-thread-local-callback-with-delay delay callback) +``` + +delay : int? + Time to delay the callback by in milliseconds + +callback : (-> any?) + Function with no arguments. + +# Examples + +```scheme +(enqueue-thread-local-callback-with-delay 1000 (lambda () (theme "focus_nova"))) ;; Run after 1 second +`` + "# + ); + + register_2!( + "helix-await-callback", + await_value, + "DEPRECATED: Please use `await-callback`" + ); + + // Arity 2 + register_2!( + "await-callback", + await_value, + r#" +Await the given value, and call the callback function on once the future is completed. + +```scheme +(await-callback future callback) +``` + +* future : future? +* callback (-> any?) + Function with no arguments"# + ); + + register_2!( + "add-inlay-hint", + add_inlay_hint, + r#" +Warning: this is experimental + +Adds an inlay hint at the given character index. Returns the (first-line, last-line) list +associated with this snapshot of the inlay hints. Use this pair of line numbers to invalidate +the inlay hints. + +```scheme +(add-inlay-hint char-index completion) -> (list int? int?) +``` + +char-index : int? +completion : string? + +"# + ); + register_2!( + "remove-inlay-hint", + remove_inlay_hint, + r#" +Warning: this is experimental and should not be used. +This will most likely be removed soon. + +Removes an inlay hint at the given character index. Note - to remove +properly, the completion must match what was already there. + +```scheme +(remove-inlay-hint char-index completion) +``` + +char-index : int? +completion : string? + +"# + ); + + register_2!( + "remove-inlay-hint-by-id", + remove_inlay_hint_by_id, + r#" +Warning: this is experimental + +Removes an inlay hint by the id that was associated with the added inlay hints. + +```scheme +(remove-inlay-hint first-line last-line) +``` + +first-line : int? +last-line : int? + +"# + ); + + if generate_sources { + if let Some(mut target_directory) = alternative_runtime_search_path() { + if !target_directory.exists() { + std::fs::create_dir_all(&target_directory).unwrap(); + } + + target_directory.push("misc.scm"); + + std::fs::write(target_directory, &builtin_misc_module).unwrap(); + } + + engine.register_steel_module("helix/misc.scm".to_string(), builtin_misc_module); + } + + if generate_sources { + configure_lsp_builtins("misc", &module); + } + + engine.register_module(module); +} + +// TODO: Generate sources into the cogs directory, so that the +// LSP can go find it. When it comes to loading though, it'll look +// up internally. +pub fn alternative_runtime_search_path() -> Option { + steel_home().map(|path| PathBuf::from(path).join("cogs").join("helix")) +} + +pub fn generate_cog_file() { + if let Some(path) = alternative_runtime_search_path() { + std::fs::write( + path.join("cog.scm"), + r#"(define package-name 'helix) + (define version "0.1.0")"#, + ) + .unwrap(); + } +} + +pub fn load_ext_api(engine: &mut Engine, generate_sources: bool) { + let ext_api = r#" +(require "helix/editor.scm") +(require "helix/misc.scm") +(require-builtin helix/core/text as text.) +(require "steel/sync") + +(provide eval-buffer + evalp + running-on-main-thread? + hx.with-context + hx.block-on-task) + +(define (get-document-as-slice) + (let* ([focus (editor-focus)] + [focus-doc-id (editor->doc-id focus)]) + (text.rope->string (editor->text focus-doc-id)))) + +;;@doc +;; Eval the current buffer, morally equivalent to load-buffer! +(define (eval-buffer) + (eval-string (get-document-as-slice))) + +;;@doc +;; Eval prompt +(define (evalp) + (push-component! (prompt "" (lambda (expr) (set-status! (eval-string expr)))))) + +;;@doc +;; Check what the main thread id is, compare to the main thread +(define (running-on-main-thread?) + (= (current-thread-id) *helix.id*)) + +;;@doc +;; If running on the main thread already, just do nothing. +;; Check the ID of the engine, and if we're already on the +;; main thread, just continue as is - i.e. just block. This does +;; not block on the function if this is running on another thread. +;; +;; ```scheme +;; (hx.with-context thunk) +;; ``` +;; thunk : (-> any?) ;; Function that has no arguments +;; +;; # Examples +;; ```scheme +;; (spawn-native-thread +;; (lambda () +;; (hx.with-context (lambda () (theme "nord"))))) +;; ``` +(define (hx.with-context thunk) + (if (running-on-main-thread?) + (thunk) + (begin + (define task (task #f)) + ;; Send on the main thread + (acquire-context-lock thunk task) + task))) + +;;@doc +;; Block on the given function. +;; ```scheme +;; (hx.block-on-task thunk) +;; ``` +;; thunk : (-> any?) ;; Function that has no arguments +;; +;; # Examples +;; ```scheme +;; (define thread +;; (spawn-native-thread +;; (lambda () +;; (hx.block-on-task (lambda () (theme "nord") 10))))) +;; +;; ;; Some time later, in a different context - if done at the same time, +;; ;; this will deadline, since the join depends on the callback previously +;; ;; executing. +;; (equal? (thread-join! thread) 10) ;; => #true +;; ``` +(define (hx.block-on-task thunk) + (if (running-on-main-thread?) (thunk) (block-on-task (hx.with-context thunk)))) + "#; + + if let Some(mut target_directory) = alternative_runtime_search_path() { + if generate_sources { + if !target_directory.exists() { + std::fs::create_dir_all(&target_directory).unwrap_or_else(|err| { + panic!("Failed to create directory {:?}: {}", target_directory, err) + }); + } + + target_directory.push("ext.scm"); + + std::fs::write(target_directory, ext_api).unwrap(); + } + } + + engine.register_steel_module("helix/ext.scm".to_string(), ext_api.to_string()); +} + +// Note: This implementation is aligned with what the steel language server +// expects. This shouldn't stay here, but for alpha purposes its fine. +pub fn steel_lsp_home_dir() -> PathBuf { + if let Ok(home) = std::env::var("STEEL_LSP_HOME") { + return PathBuf::from(home); + } + + let mut home_directory = + PathBuf::from(steel_home().expect("Unable to find steel home location")); + home_directory.push("lsp"); + + if !home_directory.exists() { + std::fs::create_dir_all(&home_directory).expect("Unable to create the lsp home directory"); + } + + home_directory +} + +// Embed them in the binary... first +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, generate_sources); + load_rope_api(engine, generate_sources); + load_misc_api(engine, generate_sources); + load_component_api(engine, generate_sources); + + // This depends on the components and theme api, so should + // be loaded after. + load_high_level_theme_api(engine, generate_sources); + load_high_level_keymap_api(engine, generate_sources); + load_ext_api(engine, generate_sources); + + // TODO: Remove this once all of the globals have been moved into their own modules + if generate_sources { + configure_lsp_globals(); + + // Generate cog file for the stubs + // that are generated and written to the $STEEL_HOME directory + generate_cog_file() + } +} + +fn acquire_context_lock( + callback_fn: SteelVal, + place: Option, +) -> steel::rvals::Result<()> { + static TASK_DONE: Lazy = Lazy::new(|| SteelVal::SymbolV("done".into())); + + 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()); + + let res = 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); + + // Set the task to be done + s.set_index(1, (*TASK_DONE).clone()); + + mutex_unlock(&lock.unwrap()).unwrap(); + } + + Err(e) => { + s.set_index(3, e.clone().into_steelval().unwrap()); + s.set_index(1, (*TASK_DONE).clone()); + mutex_unlock(&lock.unwrap()).unwrap(); + return Err(e); + } + } + } + + Ok(()) + }) + { + present_error_inside_engine_context(&mut ctx, guard, e); + } + }); + + patch_callbacks(&mut ctx); + + res + }; + job::dispatch_blocking_jobs(callback); + + Ok(()) +} + +fn configure_engine_impl(mut engine: Engine) -> Engine { + log::info!("Loading engine!"); + + // Engine: Add search directories. + engine.add_search_directory(helix_loader::config_dir()); + + for dir in helix_loader::runtime_dirs() { + engine.add_search_directory(dir.to_owned()); + } + + 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 _), + ); + + configure_builtin_sources(&mut engine, true); + + // Hooks + engine.register_fn("register-hook!", register_hook); + engine.register_fn("log::info!", |message: SteelVal| { + if let SteelVal::StringV(s) = &message { + log::info!("{}", s) + } else { + log::info!("{}", message) + } + }); + + engine.register_fn("fuzzy-match", |pattern: SteelString, items: SteelVal| { + 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::>(); + } + + Vec::new() + }); + + // Find the workspace + engine.register_fn("helix-find-workspace", || { + helix_core::find_workspace().0.to_str().unwrap().to_string() + }); + + // TODO: Deprecate the above + engine.register_fn("find-workspace", || { + helix_core::find_workspace().0.to_str().unwrap().to_string() + }); + + engine.register_fn("doc-id->usize", document_id_to_usize); + + // TODO: Remove that this is now in helix/core/misc + engine.register_fn("acquire-context-lock", acquire_context_lock); + + 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(); + + let res = 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); + } + }) + }); + + patch_callbacks(&mut ctx); + + res + }, + ); + + 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))), + } + }); + + // Experimental - use at your own risk. + engine.register_fn( + "#%exp-picker", + |values: Vec, callback_fn: SteelVal| -> 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 rooted = callback_fn.as_rooted(); + + let picker = ui::Picker::new(columns, 0, [], cwd, move |cx, path: &PathBuf, action| { + let result = cx.editor.open(path, action); + match result { + Err(e) => { + let err = if let Some(err) = e.source() { + format!("{}", err) + } else { + format!("unable to open \"{}\"", path.display()) + }; + cx.editor.set_error(err); + } + Ok(_) => { + 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 = rooted.value(); + + let res = 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); + } + }); + + patch_callbacks(&mut ctx); + + res + } + } + }) + .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); + + GLOBAL_OFFSET.store(engine.globals().len(), Ordering::Relaxed); + + 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 push_range_to_selection(cx: &mut Context, range: Range) { + let (view, doc) = current!(cx.editor); + let selection = doc.selection(view.id).clone(); + doc.set_selection(view.id, selection.push(range)) +} + +fn set_selection_primary_index(cx: &mut Context, primary_index: usize) { + let (view, doc) = current!(cx.editor); + let mut selection = doc.selection(view.id).clone(); + selection.set_primary_index(primary_index); + doc.set_selection(view.id, selection) +} + +fn remove_selection_range(cx: &mut Context, index: usize) { + let (view, doc) = current!(cx.editor); + let selection = doc.selection(view.id).clone(); + doc.set_selection(view.id, selection.remove(index)) +} + +fn current_line_number(cx: &mut Context) -> usize { + let (view, doc) = current_ref!(cx.editor); + doc.text().char_to_line( + doc.selection(view.id) + .primary() + .cursor(doc.text().slice(..)), + ) +} + +fn current_column_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(..)), + ) + .col +} + +fn current_line_character(cx: &mut Context, encoding: SteelString) -> anyhow::Result { + let (view, doc) = current_ref!(cx.editor); + + let encoding = match &***encoding { + "utf-8" => helix_lsp::OffsetEncoding::Utf8, + "utf-16" => helix_lsp::OffsetEncoding::Utf16, + "utf-32" => helix_lsp::OffsetEncoding::Utf32, + _ => anyhow::bail!("invalid encoding {encoding:?}"), + }; + + Ok(doc.position(view.id, encoding).character as usize) +} + +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()) + }) + }); + + patch_callbacks(&mut ctx); + + 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()), + } + }) + }); + + patch_callbacks(&mut ctx); + + 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 set_buffer_uri(cx: &mut Context, uri: SteelString) -> anyhow::Result<()> { + 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 { + if let Ok(url) = url::Url::from_str(uri.as_str()) { + current_doc.uri = Some(Box::new(url)); + } else { + anyhow::bail!("Unable to parse uri: {:?}", uri); + } + } + + Ok(()) +} + +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_register_value(cx: &mut Context, name: char) -> Vec { + cx.editor + .registers + .read(name, cx.editor) + .map_or(Vec::new(), |reg| reg.collect()) + .into_iter() + .map(|value| value.to_string()) + .collect() +} + +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_get_document_language(cx: &mut Context, doc_id: DocumentId) -> Option { + cx.editor + .documents + .get(&doc_id) + .and_then(|d| Some(d.language_name()?.to_string())) +} + +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 string_to_mode(_: &mut Context, value: SteelString) -> Option { + match value.as_str() { + "normal" => Some(Mode::Normal), + "insert" => Some(Mode::Insert), + "select" => Some(Mode::Select), + _ => None, + } +} + +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) { + match value { + SteelVal::StringV(s) => cx.editor.set_status(s.as_ref().to_owned()), + _ => cx.editor.set_status(value.to_string()), + } +} + +fn set_warning(cx: &mut Context, value: SteelVal) { + match value { + SteelVal::StringV(s) => cx.editor.set_warning(s.as_ref().to_owned()), + _ => cx.editor.set_warning(value.to_string()), + } +} + +fn set_error(cx: &mut Context, value: SteelVal) { + match value { + SteelVal::StringV(s) => cx.editor.set_error(s.as_ref().to_owned()), + _ => cx.editor.set_error(value.to_string()), + } +} + +fn trigger_callback(cx: &mut Context, key: KeyEvent) { + if let Some(callback) = cx.on_next_key_callback.take() { + (callback.0)(cx, key); + } +} + +fn enqueue_on_next_key(cx: &mut Context, callback_fn: SteelVal) { + let rooted = callback_fn.as_rooted(); + let current_gen = load_generation(); + + cx.on_next_key(move |ctx, key| { + let cloned_func = rooted.value(); + + enter_engine(|guard| { + if !is_current_generation(current_gen) { + return; + } + + if let Err(e) = + guard + .with_mut_reference::(ctx) + .consume(move |engine, args| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + + engine.call_function_with_args_from_mut_slice( + cloned_func.clone(), + &mut [key.into_steelval().unwrap()], + ) + }) + { + present_error_inside_engine_context(ctx, guard, e); + } + }); + }); +} + +fn enqueue_command(cx: &mut Context, callback_fn: SteelVal) { + let rooted = callback_fn.as_rooted(); + let current_gen = load_generation(); + + 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(); + + let res = enter_engine(|guard| { + if !is_current_generation(current_gen) { + return; + } + + 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); + } + }); + + patch_callbacks(&mut ctx); + + res + }, + ); + 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 current_gen = load_generation(); + + 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(); + + let res = enter_engine(|guard| { + if !is_current_generation(current_gen) { + return; + } + + 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); + } + }); + + patch_callbacks(&mut ctx); + + res + }, + ); + 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 current_gen = load_generation(); + + 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); + engine.call_function_with_args(cloned_func.clone(), vec![inner]) + }; + + let res = enter_engine(|guard| { + if !is_current_generation(current_gen) { + return; + } + + if let Err(e) = guard + .with_mut_reference::(&mut ctx) + .consume_once(callback) + { + present_error_inside_engine_context(&mut ctx, guard, e); + } + }); + + patch_callbacks(&mut ctx); + + res + } + 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(&path); + if !path.exists() { + 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(); + + selection.primary().cursor(text) +} + +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 { + let loader: &helix_core::syntax::Loader = &cx.editor.syn_loader.load(); + // 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, loader, view) + .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() + {} +} + +#[derive(Debug, Clone)] +struct LspClient(Weak); + +impl LspClient { + fn new(client: Arc) -> Self { + LspClient(Arc::downgrade(&client)) + } +} + +impl Custom for LspClient {} + +fn get_active_lsp_clients(cx: &mut Context) -> SteelVal { + let (_, doc) = current!(cx.editor); + SteelVal::ListV( + doc.arc_language_servers() + .map(|client| LspClient::new(client).into_steelval().unwrap()) + .collect(), + ) +} + +fn is_lsp_client_initialized(client: LspClient) -> bool { + let client = client.0.upgrade(); + client.is_some_and(|client| client.is_initialized()) +} + +fn lsp_client_name(client: LspClient) -> Option { + let client = client.0.upgrade(); + client.map(|client| client.name().to_owned()) +} + +fn lsp_client_offset_encoding(client: LspClient) -> Option<&'static str> { + let client = client.0.upgrade(); + client + .filter(|client| client.is_initialized()) + .map(|client| match client.offset_encoding() { + helix_lsp::OffsetEncoding::Utf8 => "utf-8", + helix_lsp::OffsetEncoding::Utf16 => "utf-16", + helix_lsp::OffsetEncoding::Utf32 => "utf-32", + }) +} + +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 lsp_reply_ok( + cx: &mut Context, + name: SteelString, + id: SteelString, + result: SteelVal, +) -> anyhow::Result<()> { + let serde_value: Result = result.try_into(); + let value = match serde_value { + Ok(serialized_value) => serialized_value, + Err(error) => { + log::warn!("Failed to serialize a SteelVal: {}", error); + serde_json::Value::Null + } + }; + + 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(); + + cx.editor + .language_server_by_id(language_server_id) + .ok_or(anyhow::anyhow!("Failed to find a language server by id")) + .and_then(|language_server| { + language_server + .reply(jsonrpc::Id::Str(id.to_string()), Ok(value)) + .map_err(Into::into) + }) +} + +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) => { + let res = 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()], + ) + }); + + patch_callbacks(&mut ctx); + + if let Err(e) = res { + 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(()) +} + +// "add-inlay-hint", +pub fn add_inlay_hint( + cx: &mut Context, + char_index: usize, + completion: SteelString, +) -> Option<(usize, usize)> { + let view_id = cx.editor.tree.focus; + if !cx.editor.tree.contains(view_id) { + return None; + } + let view = cx.editor.tree.get(view_id); + let doc_id = cx.editor.tree.get(view_id).doc; + let doc = cx.editor.documents.get_mut(&doc_id)?; + let mut new_inlay_hints = doc.inlay_hints(view_id).cloned().unwrap_or_else(|| { + let doc_text = doc.text(); + let len_lines = doc_text.len_lines(); + + let view_height = view.inner_height(); + let first_visible_line = + doc_text.char_to_line(doc.view_offset(view_id).anchor.min(doc_text.len_chars())); + let first_line = first_visible_line.saturating_sub(view_height); + let last_line = first_visible_line + .saturating_add(view_height.saturating_mul(2)) + .min(len_lines); + + let new_doc_inlay_hints_id = DocumentInlayHintsId { + first_line, + last_line, + }; + + DocumentInlayHints::empty_with_id(new_doc_inlay_hints_id) + }); + + // TODO: The inlay hints should actually instead return the id? + new_inlay_hints + .other_inlay_hints + .push(InlineAnnotation::new(char_index, completion.to_string())); + + let id = new_inlay_hints.id; + + doc.set_inlay_hints(view_id, new_inlay_hints); + + Some((id.first_line, id.last_line)) +} + +pub fn remove_inlay_hint_by_id( + cx: &mut Context, + first_line: usize, + last_line: usize, +) -> Option<()> { + // let text = completion.to_string(); + let view_id = cx.editor.tree.focus; + if !cx.editor.tree.contains(view_id) { + return None; + } + let view = cx.editor.tree.get(view_id); + let doc_id = cx.editor.tree.get(view_id).doc; + let doc = cx.editor.documents.get_mut(&doc_id)?; + + let inlay_hints = doc.inlay_hints(view_id)?; + let id = DocumentInlayHintsId { + first_line, + last_line, + }; + + if inlay_hints.id == id { + let doc_text = doc.text(); + let len_lines = doc_text.len_lines(); + + let view_height = view.inner_height(); + let first_visible_line = + doc_text.char_to_line(doc.view_offset(view_id).anchor.min(doc_text.len_chars())); + let first_line = first_visible_line.saturating_sub(view_height); + let last_line = first_visible_line + .saturating_add(view_height.saturating_mul(2)) + .min(len_lines); + + let new_doc_inlay_hints_id = DocumentInlayHintsId { + first_line, + last_line, + }; + + doc.set_inlay_hints( + view_id, + DocumentInlayHints::empty_with_id(new_doc_inlay_hints_id), + ); + + return Some(()); + } + + None +} + +// "remove-inlay-hint", +pub fn remove_inlay_hint(cx: &mut Context, char_index: usize, _completion: SteelString) -> bool { + // let text = completion.to_string(); + let view_id = cx.editor.tree.focus; + if !cx.editor.tree.contains(view_id) { + return false; + } + let doc_id = cx.editor.tree.get_mut(view_id).doc; + let doc = match cx.editor.documents.get_mut(&doc_id) { + Some(x) => x, + None => return false, + }; + + let inlay_hints = match doc.inlay_hints(view_id) { + Some(inlay_hints) => inlay_hints, + None => return false, + }; + let mut new_inlay_hints = inlay_hints.clone(); + new_inlay_hints + .other_inlay_hints + .retain(|x| x.char_idx != char_index); + doc.set_inlay_hints(view_id, new_inlay_hints); + true +} + +pub fn insert_string(cx: &mut Context, string: SteelString) { + let (view, doc) = current!(cx.editor); + + let indent = Tendril::from(string.as_str()); + let transaction = Transaction::insert( + doc.text(), + &doc.selection(view.id).clone().cursors(doc.text().slice(..)), + indent, + ); + doc.apply(&transaction, view.id); +} diff --git a/helix-term/src/commands/engine/steel/themes.scm b/helix-term/src/commands/engine/steel/themes.scm new file mode 100644 index 000000000000..a92a321c32b2 --- /dev/null +++ b/helix-term/src/commands/engine/steel/themes.scm @@ -0,0 +1,382 @@ +(require-builtin helix/core/themes) +(require-builtin helix/components) + +(provide attribute + type + type.builtin + type.parameter + type.enum + type.enum.variant + constructor + constant + constant.builtin + constant.builtin.boolean + constant.character + constant.character.escape + constant.numeric + constant.numeric.integer + constant.numeric.float + string + string.regexp + string.special + string.special.path + string.special.url + string.special.symbol + comment + comment.line + comment.block + comment.block.documentation + variable + variable.builtin + variable.parameter + variable.other + variable.other.member + variable.other.member.private + label + punctuation + punctuation.delimiter + punctuation.bracket + punctuation.special + keyword + keyword.control + keyword.control.conditional + keyword.control.repeat + keyword.control.import + keyword.control.return + keyword.control.exception + keyword.operator + keyword.directive + keyword.function + keyword.storage + keyword.storage.type + keyword.storage.modifier + operator + function + function.builtin + function.method + function.method.private + function.macro + function.special + tag + tag.builtin + namespace + special + markup + markup.heading + markup.heading.marker + markup.heading.marker.1 + markup.heading.marker.2 + markup.heading.marker.3 + markup.heading.marker.4 + markup.heading.marker.5 + markup.heading.marker.6 + markup.list + markup.list.unnumbered + markup.list.numbered + markup.list.checked + markup.list.unchecked + markup.bold + markup.italic + markup.strikethrough + markup.link + markup.link.url + markup.link.label + markup.link.text + markup.quote + markup.raw + markup.raw.inline + markup.raw.block + diff + diff.plus + diff.plus.gutter + diff.minus + diff.minus.gutter + diff.delta + diff.delta.moved + diff.delta.conflict + diff.delta.gutter + markup.normal.completion + markup.normal.hover + markup.heading.completion + markup.heading.hover + markup.raw.inline.completion + markup.raw.inline.hover + ui.background + ui.background.separator + ui.cursor + ui.cursor.insert + ui.cursor.select + ui.cursor.match + ui.cursor.primary + ui.cursor.primary.normal + ui.cursor.primary.insert + ui.cursor.primary.select + ui.debug.breakpoint + ui.debug.active + ui.gutter + ui.gutter.selected + ui.highlight.frameline + ui.linenr + ui.linenr.selected + ui.statusline + ui.statusline.inactive + ui.statusline.normal + ui.statusline.insert + ui.statusline.select + ui.statusline.separator + ui.bufferline + ui.bufferline.active + ui.bufferline.background + ui.popup + ui.popup.info + ui.window + ui.help + ui.text + ui.text.focus + ui.text.inactive + ui.text.info + ui.virtual.ruler + ui.virtual.whitespace + ui.virtual.indent-guide + ui.virtual.inlay-hint + ui.virtual.inlay-hint.parameter + ui.virtual.inlay-hint.type + ui.virtual.wrap + ui.virtual.jump-label + ui.menu + ui.menu.selected + ui.menu.scroll + ui.selection + ui.selection.primary + ui.highlight + ui.cursorline + ui.cursorline.primary + ui.cursorline.secondary + ui.cursorcolumn.primary + ui.cursorcolumn.secondary + warning + error + info + hint + diagnostic + diagnostic.hint + diagnostic.info + diagnostic.warning + diagnostic.error + diagnostic.unnecessary + diagnostic.deprecated) + +(provide hashmap->theme + register-theme + theme-style + theme-set-style! + string->color) + +;;@doc +;; Register this theme with helix for use +(define (register-theme theme) + (add-theme! *helix.cx* theme)) + +(define-syntax theme-func + (syntax-rules () + [(_ scope doc-string) + (@doc doc-string + (define (scope theme style) + (theme-set-style! theme (quote scope) style) + theme))] + + [(_ scope) + (define (scope theme style) + (theme-set-style! theme (quote scope) style) + theme)])) + +(theme-func attribute "Class attributes, HTML tag attributes") +(theme-func type "Types") +(theme-func type.builtin "Primitive types provided by the language (`int`, `usize`)") +(theme-func type.parameter "Generic type parameters (`T`)") +(theme-func type.enum "Enum usage") +(theme-func type.enum.variant "Enum variant") +(theme-func constructor "Constructor usage") +(theme-func constant "Constants usage") +(theme-func constant.builtin + "Special constants provided by the language (`true`, `false`, `nil`, etc)") +(theme-func constant.builtin.boolean "A special case for highlighting individual booleans") +(theme-func constant.character "Character usage") +(theme-func constant.character.escape "Highlighting individual escape characters") +(theme-func constant.numeric "Numbers") +(theme-func constant.numeric.integer "Integers") +(theme-func constant.numeric.float "Floats") +(theme-func string "Highlighting strings") +(theme-func string.regexp "Highlighting regular expressions") +(theme-func string.special "Special strings") +(theme-func string.special.path "Highlighting paths") +(theme-func string.special.url "Highlighting URLs") +(theme-func string.special.symbol "Erlang/Elixir atoms, Ruby symbols, Clojure keywords") +(theme-func comment "Highlighting comments") +(theme-func comment.line "Single line comments (`//`)") +(theme-func comment.block "Block comments (`/* */`)") +(theme-func comment.block.documentation "Documentation comments (e.g. `///` in Rust)") +(theme-func variable "Variables") +(theme-func variable.builtin "Reserved language variables (`self`, `this`, `super`, etc.)") +(theme-func variable.parameter "Function parameters") +(theme-func variable.other "Other variables") +(theme-func variable.other.member "Fields of composite data types (e.g. structs, unions)") +(theme-func variable.other.member.private + "Private fields that use a unique syntax (currently just EMCAScript-based languages)") + +(theme-func label "Highlighting labels") +(theme-func punctuation "Highlighting punctuation") +(theme-func punctuation.delimiter "Commas, colon") +(theme-func punctuation.bracket "Parentheses, angle brackets, etc.") +(theme-func punctuation.special "String interpolation brackets") + +(theme-func keyword "Highlighting keywords") +(theme-func keyword.control "Control keywords") +(theme-func keyword.control.conditional "if, else") +(theme-func keyword.control.repeat "for, while, loop") +(theme-func keyword.control.import "import, export") +(theme-func keyword.control.return "return keyword") +(theme-func keyword.control.exception "exception keyword") + +(theme-func keyword.operator "or, in") +(theme-func keyword.directive "Preprocessor directives (`#if` in C)") +(theme-func keyword.function "fn, func") +(theme-func keyword.storage "Keywords describing how things are stored") +(theme-func keyword.storage.type "The type of something, `class`, `function`, `var`, `let`, etc") +(theme-func keyword.storage.modifier "Storage modifiers like `static`, `mut`, `const`, `ref`, etc") + +(theme-func operator "Operators such as `||`, `+=`, `>`, etc") +(theme-func function "Highlighting function calls") +(theme-func function.builtin "Builtin functions") +(theme-func function.method "Calling methods") +(theme-func function.method.private + "Private methods that use a unique syntax (currently just ECMAScript-based languages)") +(theme-func function.macro "Highlighting macros") +(theme-func function.special "Preprocessor in C") + +(theme-func tag "Tags (e.g. in HTML)") +(theme-func tag.builtin "Builtin tags") + +(theme-func namespace) +(theme-func special) +(theme-func markup "Highlighting markdown") +(theme-func markup.heading "Markdown heading") +(theme-func markup.heading.marker "Markdown heading marker") +(theme-func markup.heading.marker.1 "Markdown heading text h1") +(theme-func markup.heading.marker.2 "Markdown heading text h2") +(theme-func markup.heading.marker.3 "Markdown heading text h3") +(theme-func markup.heading.marker.4 "Markdown heading text h4") +(theme-func markup.heading.marker.5 "Markdown heading text h5") +(theme-func markup.heading.marker.6 "Markdown heading text h6") + +(theme-func markup.list "Markdown lists") +(theme-func markup.list.unnumbered "Unnumbered markdown lists") +(theme-func markup.list.numbered "Numbered markdown lists") +(theme-func markup.list.checked "Checked markdown lists") +(theme-func markup.list.unchecked "Unchecked markdown lists") + +(theme-func markup.bold "Markdown bold") +(theme-func markup.italic "Markdown italics") +(theme-func markup.strikethrough "Markdown strikethrough") +(theme-func markup.link "Markdown links") +(theme-func markup.link.url "URLs pointed to by links") +(theme-func markup.link.label "non-URL link references") +(theme-func markup.link.text "URL and image descriptions in links") +(theme-func markup.quote "Markdown quotes") +(theme-func markup.raw "Markdown raw") +(theme-func markup.raw.inline "Markdown inline raw") +(theme-func markup.raw.block "Markdown raw block") + +(theme-func diff "Version control changes") +(theme-func diff.plus "Version control additions") +(theme-func diff.plus.gutter "Version control addition gutter indicator") +(theme-func diff.minus "Version control deletions") +(theme-func diff.minus.gutter "Version control deletion gutter indicator") +(theme-func diff.delta "Version control modifications") +(theme-func diff.delta.moved "Renamed or moved files/changes") +(theme-func diff.delta.conflict "Merge conflicts") +(theme-func diff.delta.gutter "Gutter indicator") + +(theme-func markup.normal.completion "For completion doc popup UI") +(theme-func markup.normal.hover "For hover popup UI") +(theme-func markup.heading.completion "For completion doc popup UI") +(theme-func markup.heading.hover "For hover popup UI") +(theme-func markup.raw.inline.completion "For completion doc popup UI") +(theme-func markup.raw.inline.hover "For hover popup UI") + +(theme-func ui.background) +(theme-func ui.background.separator "Picker separator below input line") +(theme-func ui.cursor) +(theme-func ui.cursor.normal) +(theme-func ui.cursor.insert) +(theme-func ui.cursor.select) +(theme-func ui.cursor.match "Matching bracket etc.") +(theme-func ui.cursor.primary "Cursor with primary selection") +(theme-func ui.cursor.primary.normal) +(theme-func ui.cursor.primary.insert) +(theme-func ui.cursor.primary.select) + +(theme-func ui.debug.breakpoint "Breakpoint indicator, found in the gutter") +(theme-func ui.debug.active + "Indicator for the line at which debugging execution is paused at, found in the gutter") +(theme-func ui.gutter "Gutter") +(theme-func ui.gutter.selected "Gutter for the line the cursor is on") +(theme-func ui.highlight.frameline "Line at which debugging execution is paused at") +(theme-func ui.linenr "Line numbers") +(theme-func ui.linenr.selected "Line number for the line the cursor is on") +(theme-func ui.statusline "Statusline") +(theme-func ui.statusline.inactive "Statusline (unfocused document)") +(theme-func ui.statusline.normal + "Statusline mode during normal mode (only if editor.color-modes is enabled)") +(theme-func ui.statusline.insert + "Statusline mode during insert mode (only if editor.color-modes is enabled)") +(theme-func ui.statusline.select + "Statusline mode during select mode (only if editor.color-modes is enabled)") + +(theme-func ui.statusline.separator "Separator character in statusline") +(theme-func ui.bufferline "Style for the buffer line") +(theme-func ui.bufferline.active "Style for the active buffer in buffer line") +(theme-func ui.bufferline.background "Style for the bufferline background") +(theme-func ui.popup "Documentation popups (e.g. Space + k)") +(theme-func ui.popup.info "Prompt for multiple key options") +(theme-func ui.window "Borderline separating splits") +(theme-func ui.help "Description box for commands") +(theme-func ui.text "Default text style, command prompts, popup text, etc.") +(theme-func ui.text.focus "The currently selected line in the picker") +(theme-func ui.text.inactive "Same as ui.text but when the text is inactive (e.g. suggestions)") +(theme-func ui.text.info "The key: command text in ui.popup.info boxes") +(theme-func ui.virtual.ruler "Ruler columns (see the editor.rules config)") +(theme-func ui.virtual.whitespace "Visible whitespace characters") +(theme-func ui.virtual.indent-guide "Vertical indent width guides") +(theme-func ui.virtual.inlay-hint "Default style for inlay hints of all kinds") +(theme-func ui.virtual.inlay-hint.parameter + "Style for inlay hints of kind `parameter` (LSPs are not rquired to set a kind)") +(theme-func ui.virtual.inlay-hint.type + "Style for inlay hints of kind `type` (LSPs are not required to set a kind)") +(theme-func ui.virtual.wrap "Soft-wrap indicator (see the editor.soft-wrap config)") +(theme-func ui.virtual.jump-label "Style for virtual jump labels") +(theme-func ui.menu "Code and command completion menus") +(theme-func ui.menu.selected "Selected autocomplete item") +(theme-func ui.menu.scroll "fg sets thumb color, bg sets track color of scrollbar") +(theme-func ui.selection "For selections in the editing area") +(theme-func ui.selection.primary) +(theme-func ui.highlight "Highlighted lines in the picker preview") +(theme-func ui.cursorline "The line of the cursor (if cursorline is enabled)") +(theme-func ui.cursorline.primary "The line of the primary cursor (if cursorline is enabled)") +(theme-func ui.cursorline.secondary "The line of the secondary cursor (if cursorline is enabled)") +(theme-func ui.cursorcolumn.primary "The column of the primary cursor (if cursorcolumn is enabled)") +(theme-func ui.cursorcolumn.secondary + "The column of the secondary cursor (if cursorcolumn is enabled)") + +(theme-func warning "Diagnostics warning (gutter)") +(theme-func error "Diagnostics error (gutter)") +(theme-func info "Diagnostics info (gutter)") +(theme-func hint "Diagnostics hint (gutter)") + +(theme-func diagnostic "Diagnostics fallback style (editing area)") +(theme-func diagnostic.hint "Diagnostics hint (editing area)") +(theme-func diagnostic.info "Diagnostics info (editing area)") +(theme-func diagnostic.warning "Diagnostics warning (editing area)") +(theme-func diagnostic.error "Diagnostics error (editing area)") +(theme-func diagnostic.unnecessary "Diagnostics with unnecessary tag (editing area)") +(theme-func diagnostic.deprecated "Diagnostics with deprecated tag (editing area)") diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index b928dd4f28d0..29ef9236bc98 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, @@ -39,21 +40,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, @@ -776,6 +777,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(); @@ -828,7 +831,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"); } @@ -1037,7 +1046,7 @@ fn theme(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow bail!("Unsupported theme: theme requires true color support"); } cx.editor.set_theme_preview(theme); - }; + } }; } PromptEvent::Validate => { @@ -1046,7 +1055,8 @@ fn theme(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow .editor .theme_loader .load(theme_name) - .map_err(|err| anyhow::anyhow!("Could not load theme: {}", err))?; + .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"); } @@ -3818,10 +3828,40 @@ fn execute_command_line( return execute_command(cx, cmd, command, event); } - 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 => Ok(()), + if event == PromptEvent::Validate { + let parts = rest.split_whitespace().collect::>(); + if ScriptingEngine::call_typed_command(cx, command, &parts, event) { + // Engine handles the other cases + let mappable_command = MappableCommand::Typable { + name: command.to_string(), + args: parts.join(" "), + 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, + }; + + helix_event::dispatch(crate::events::PostCommand { + command: &mappable_command, + cx: &mut ctx, + }); + + return Ok(()); + } else if let Some(cmd) = typed::TYPABLE_COMMAND_MAP.get(command) { + execute_command(cx, cmd, rest, event) + } else { + Err(anyhow!("1 no such command: '{command}'")) + } + } else if let Some(cmd) = typed::TYPABLE_COMMAND_MAP.get(command) { + execute_command(cx, cmd, rest, event) + } else { + Ok(()) } } @@ -3841,7 +3881,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)] @@ -3863,9 +3926,16 @@ pub(super) fn command_mode(cx: &mut Context) { cx.push_layer(Box::new(prompt)); } -fn command_line_doc(input: &str) -> Option> { - let (command, _, _) = command_line::split(input); - let command = TYPABLE_COMMAND_MAP.get(command)?; +fn command_line_doc(input: &str) -> Option> { + let (command_name, _, _) = command_line::split(input); + + if let Some(doc) = ScriptingEngine::get_doc_for_identifier(command_name).map(|x| x.into()) { + return Some(doc); + } + + let command = TYPABLE_COMMAND_MAP.get(command_name); + + let command = command?; if command.aliases.is_empty() && command.signature.flags.is_empty() { return Some(Cow::Borrowed(command.doc)); @@ -3941,7 +4011,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> { + self.id() + } } pub struct Compositor { @@ -136,11 +141,20 @@ 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 remove_type(&mut self) { let type_name = std::any::type_name::(); self.layers .retain(|component| component.type_name() != type_name); } + 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. @@ -167,6 +181,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); } @@ -183,7 +201,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/events.rs b/helix-term/src/events.rs index b0a42298914d..cbd5ee004b46 100644 --- a/helix-term/src/events.rs +++ b/helix-term/src/events.rs @@ -2,7 +2,8 @@ use helix_event::{events, register_event}; use helix_view::document::Mode; use helix_view::events::{ ConfigDidChange, DiagnosticsDidChange, DocumentDidChange, DocumentDidClose, DocumentDidOpen, - DocumentFocusLost, LanguageServerExited, LanguageServerInitialized, SelectionDidChange, + DocumentFocusLost, DocumentSaved, LanguageServerExited, LanguageServerInitialized, + SelectionDidChange, }; use crate::commands; @@ -22,6 +23,7 @@ pub fn register() { register_event::(); register_event::(); register_event::(); + register_event::(); register_event::(); register_event::(); register_event::(); 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..7383b400bb4f 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 next.map.values_mut() { + 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), @@ -318,7 +335,7 @@ impl Keymaps { } pub fn contains_key(&self, mode: Mode, key: KeyEvent) -> bool { - let keymaps = &*self.map(); + let keymaps = self.map.load(); let keymap = &keymaps[&mode]; keymap .search(self.pending()) @@ -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 { @@ -438,6 +464,7 @@ mod tests { assert_ne!(keymap, merged_keyamp); let mut keymap = Keymaps::new(Box::new(Constant(merged_keyamp.clone()))); + assert_eq!( keymap.get(Mode::Normal, key!('i')), KeymapResult::Matched(MappableCommand::normal_mode), diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index bdca0c012f8d..3920f51b9e17 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -3,6 +3,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(); @@ -39,7 +40,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()); @@ -116,6 +117,11 @@ FLAGS: setup_logging(args.verbosity).context("failed to initialize logging")?; + // 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 de85268a6c23..f6ee500a34b2 100644 --- a/helix-term/src/ui/document.rs +++ b/helix-term/src/ui/document.rs @@ -46,6 +46,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 b25af107d796..afc88efc7d75 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, @@ -870,7 +870,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| { @@ -1535,6 +1539,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..fad86182ebd0 --- /dev/null +++ b/helix-term/src/ui/extension.rs @@ -0,0 +1,19 @@ +#[cfg(feature = "steel")] +mod steel_implementations { + + use crate::{ + compositor::Component, + ui::{overlay::Overlay, Popup, Prompt, Text}, + }; + + impl steel::rvals::Custom for Text {} + impl steel::rvals::Custom for Popup {} + + // 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 {} + unsafe impl Send for Prompt {} + unsafe impl Sync for Prompt {} +} diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 58b6fc008ac6..b87467b3365e 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; @@ -445,13 +446,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.theme_loader.dynamic_themes().map(|x| x.into())); + names.sort(); names.dedup(); diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 88c75fe12634..a85022a0d2c8 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -451,6 +451,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) }; @@ -508,6 +509,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-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index 567422c397d3..a1f0c7e86314 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -10,6 +10,7 @@ use helix_core::{diagnostic::Severity, test, Selection, Transaction}; use helix_term::{application::Application, args::Args, config::Config, keymap::merge_keys}; use helix_view::{current_ref, doc, editor::LspConfig, input::parse_macro, Editor}; use tempfile::NamedTempFile; +use termina::event::{Event, KeyEvent}; use tokio_stream::wrappers::UnboundedReceiverStream; #[cfg(windows)] diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml index bcf890f50073..e3274bf5534a 100644 --- a/helix-tui/Cargo.toml +++ b/helix-tui/Cargo.toml @@ -13,6 +13,7 @@ homepage.workspace = true [features] default = ["termina", "crossterm"] +steel = ["dep:steel-core", "helix-view/steel", "helix-core/steel"] [dependencies] helix-view = { path = "../helix-view", features = ["term"] } @@ -25,6 +26,7 @@ termina = { workspace = true, optional = true } termini = "1.0" once_cell = "1.21" log = "~0.4" +steel-core = { workspace = true, optional = true } [target.'cfg(windows)'.dependencies] crossterm = { version = "0.28", 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 91e7d7bdbfc4..c3b4d7834003 100644 --- a/helix-tui/src/lib.rs +++ b/helix-tui/src/lib.rs @@ -1,5 +1,6 @@ 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 24dd0f2aaab2..efc5e3a9105f 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -13,6 +13,7 @@ homepage.workspace = true [features] default = [] term = ["termina", "crossterm"] +steel = ["dep:steel-core", "helix-core/steel"] unicode-lines = [] [dependencies] @@ -49,6 +50,9 @@ serde_json = "1.0" toml.workspace = true 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 36fc3524917b..d0bb48ed2312 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -202,6 +202,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, pub previous_diagnostic_id: Option, @@ -211,6 +213,9 @@ pub struct Document { // NOTE: ideally this would live on the handler for color swatches. This is blocked on a // large refactor that would make `&mut Editor` available on the `DocumentDidChange` event. pub color_swatch_controller: TaskController, + + pub uri: Option>, + pub pull_diagnostic_controller: TaskController, // NOTE: this field should eventually go away - we should use the Editor's syn_loader instead @@ -726,10 +731,12 @@ impl Document { config, version_control_head: None, focused_at: std::time::Instant::now(), + name: None, readonly: false, jump_labels: HashMap::new(), color_swatches: None, color_swatch_controller: TaskController::new(), + uri: None, syn_loader, previous_diagnostic_id: None, pull_diagnostic_controller: TaskController::new(), @@ -1186,6 +1193,10 @@ impl Document { } } + pub fn last_saved_time(&self) -> SystemTime { + self.last_saved_time + } + pub fn pickup_last_saved_time(&mut self) { self.last_saved_time = match self.path() { Some(path) => match path.metadata() { @@ -1827,6 +1838,20 @@ impl Document { .unwrap_or_else(|| self.config.load().path_completion) } + #[cfg(feature = "steel")] + pub fn arc_language_servers(&self) -> impl Iterator> + use<'_> { + self.language_config().into_iter().flat_map(move |config| { + config.language_servers.iter().filter_map(move |features| { + let ls = self.language_servers.get(&features.name)?.clone(); + if ls.is_initialized() { + Some(ls) + } else { + None + } + }) + }) + } + /// maintains the order as configured in the language_servers TOML array pub fn language_servers(&self) -> impl Iterator { self.language_config().into_iter().flat_map(move |config| { @@ -1945,7 +1970,10 @@ impl Document { /// File path as a URL. pub fn url(&self) -> Option { - Url::from_file_path(self.path()?).ok() + self.uri + .as_ref() + .map(|x| *x.clone()) + .or_else(|| Url::from_file_path(self.path()?).ok()) } pub fn uri(&self) -> Option { @@ -2001,7 +2029,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 7f8cff9c3e44..47f0791e922f 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -4,7 +4,7 @@ use crate::{ document::{ DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint, }, - events::{DocumentDidClose, DocumentDidOpen, DocumentFocusLost}, + events::{DocumentDidClose, DocumentDidOpen, DocumentFocusLost, DocumentSaved}, graphics::{CursorKind, Rect}, handlers::Handlers, info::Info, @@ -427,6 +427,10 @@ pub struct Config { pub rainbow_brackets: bool, /// Whether to enable Kitty Keyboard Protocol pub kitty_keyboard_protocol: KittyKeyboardProtocolConfig, + + /// Whether or not to use steel for configuration. Defaults to `true`. If set to `false`, + /// the steel engine will not be initialized. + pub enable_steel: bool, } #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, Clone, Copy)] @@ -696,6 +700,10 @@ pub enum StatusLineElement { pub struct CursorShapeConfig([CursorKind; 3]); impl CursorShapeConfig { + pub fn update(&mut self, mode: Mode, kind: CursorKind) { + self.0[mode as usize] = kind; + } + pub fn from_mode(&self, mode: Mode) -> CursorKind { self.get(mode as usize).copied().unwrap_or_default() } @@ -1118,6 +1126,12 @@ impl Default for Config { editor_config: true, rainbow_brackets: false, kitty_keyboard_protocol: Default::default(), + + #[cfg(feature = "steel")] + enable_steel: true, + + #[cfg(not(feature = "steel"))] + enable_steel: false, } } } @@ -1219,6 +1233,16 @@ pub struct Editor { pub mouse_down_range: Option, pub cursor_cache: CursorCache, + + pub editor_clipping: ClippingConfiguration, +} + +#[derive(Default)] +pub struct ClippingConfiguration { + pub top: Option, + pub bottom: Option, + pub left: Option, + pub right: Option, } pub type Motion = Box; @@ -1237,6 +1261,7 @@ pub enum EditorEvent { pub enum ConfigEvent { Refresh, Update(Box), + Change, } enum ThemeAction { @@ -1340,6 +1365,7 @@ impl Editor { handlers, mouse_down_range: None, cursor_cache: CursorCache::default(), + editor_clipping: ClippingConfiguration::default(), } } @@ -1710,6 +1736,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; @@ -2034,6 +2062,11 @@ impl Editor { self.write_count += 1; + dispatch(DocumentSaved { + editor: self, + doc: doc_id, + }); + Ok(()) } diff --git a/helix-view/src/events.rs b/helix-view/src/events.rs index 0435e6a47197..e85a84b07c8c 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..b85856bacc1b --- /dev/null +++ b/helix-view/src/extension.rs @@ -0,0 +1,130 @@ +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, KeyEvent}, + Document, DocumentId, Editor, ViewId, + }; + + impl steel::gc::unsafe_erased_pointers::CustomReference for Editor {} + steel::custom_reference!(Editor); + + impl steel::rvals::Custom for Mode { + 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 { + steel::SteelVal::StringV(steel_string) | steel::SteelVal::SymbolV(steel_string) => { + matches!( + (self, steel_string.as_str()), + (Self::Normal, "normal") + | (Self::Insert, "insert") + | (Self::Select, "select") + ) + } + _ => false, + } + } + } + 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 { + 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 DocumentId { + 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 ViewId { + fn equality_hint(&self, other: &dyn steel::rvals::CustomType) -> bool { + if let Some(other) = as_underlying_type::(other) { + self == other + } else { + false + } + } + } + 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 {} + + impl Custom for KeyEvent { + fn equality_hint(&self, other: &dyn steel::rvals::CustomType) -> bool { + if let Some(other) = as_underlying_type::(other) { + self == other + } else { + false + } + } + } +} diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index 3a4eee3db7ba..4b5794c00df2 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -343,6 +343,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..fa5c4d4505d4 100644 --- a/helix-view/src/info.rs +++ b/helix-view/src/info.rs @@ -40,18 +40,26 @@ impl Info { let mut text = String::new(); 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 + ); + } + + for line in line_iter { + let _ = writeln!(text, "{:width$} {}", "", line, width = item_width); + } } 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 a7e9f4618c91..a7108fa891ef 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -18,6 +18,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 173a40f3f9f9..aeb671b6967e 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -108,6 +108,9 @@ impl<'de> Deserialize<'de> for Config { pub struct Loader { /// Theme directories to search from highest to lowest priority theme_dirs: Vec, + + /// Themes which are dynamically created at runtime + dynamic_themes: HashMap, } impl Loader { /// Creates a new loader that can load themes from multiple directories. @@ -117,18 +120,34 @@ impl Loader { pub fn new(dirs: &[PathBuf]) -> Self { Self { theme_dirs: dirs.iter().map(|p| p.join("themes")).collect(), + dynamic_themes: HashMap::new(), } } + pub fn dynamic_themes(&self) -> impl Iterator { + self.dynamic_themes.keys() + } + /// Loads a theme searching directories in priority order. pub fn load(&self, name: &str) -> Result { - let (theme, warnings) = self.load_with_warnings(name)?; + match self.load_with_warnings(name) { + Ok((theme, warnings)) => { + for warning in warnings { + warn!("Theme '{}': {}", name, warning); + } - for warning in warnings { - warn!("Theme '{}': {}", name, warning); + Ok(theme) + } + Err(_) => self + .dynamic_themes + .get(name) + .ok_or_else(|| anyhow::anyhow!("Could not load theme: {}", name)) + .cloned(), } + } - Ok(theme) + pub fn add_dynamic_theme(&mut self, name: String, theme: Theme) { + self.dynamic_themes.insert(name, theme); } /// Loads a theme searching directories in priority order, returning any warnings @@ -439,10 +458,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. @@ -495,7 +531,7 @@ impl Theme { self.rainbow_length } - 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 { @@ -519,7 +555,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 d596b35a7554..66da8abe2010 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/languages.toml b/languages.toml index 812fb1d8174b..6138846832c0 100644 --- a/languages.toml +++ b/languages.toml @@ -4933,7 +4933,7 @@ indent = { tab-width = 2, unit = " " } block-comment-tokens = { start = "" } word-completion.trigger-length = 4 -[[language]] +[[language]] name = "slisp" scope = "source.sl" injection-regex = "sl" diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 74c6fc7a2d28..f0c6c4d3ca94 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.82.0" +channel = "1.84.0" components = ["rustfmt", "rust-src", "clippy"] diff --git a/steel-docs.md b/steel-docs.md new file mode 100644 index 000000000000..fc480b5d3827 --- /dev/null +++ b/steel-docs.md @@ -0,0 +1,3318 @@ +# /home/matt/.steel/cogs/helix/keymaps.scm +### ***reverse-buffer-map-insert*** +Insert a value into the reverse buffer map +### **set-global-buffer-or-extension-keymap** +Check that the types on this map check out, otherwise we don't need to consistently do these checks +### **query-global-keymap** +Query the global keybindings. + +```scheme +(query-global-keymap "normal" '("space" "f")) ;; => "file_picker" +``` +### **add-global-keybinding** +Add keybinding to the global default +### **deep-copy-global-keybindings** +Deep copy the global keymap +### **keymap** +# /home/matt/.steel/cogs/helix/configuration.scm +### **statusline** +Configuration of the statusline elements. +The following status line elements can be configured: + +Key Description +------------------------------------------------------------------------------------------- +mode The current editor mode (mode.normal/mode.insert/mode.select) +spinner A progress spinner indicating LSP activity +file-name The path/name of the opened file +file-absolute-path The absolute path/name of the opened file +file-base-name The basename of the opened file +file-modification-indicator The indicator to show whether the file is modified (a [+] appears when there are unsaved changes) +file-encoding The encoding of the opened file if it differs from UTF-8 +file-line-ending The file line endings (CRLF or LF) +file-indent-style The file indentation style +read-only-indicator An indicator that shows [readonly] when a file cannot be written +total-line-numbers The total line numbers of the opened file +file-type The type of the opened file +diagnostics The number of warnings and/or errors +workspace-diagnostics The number of warnings and/or errors on workspace +selections The primary selection index out of the number of active selections +primary-selection-length The number of characters currently in primary selection +position The cursor position +position-percentage The cursor position as a percentage of the total number of lines +separator The string defined in editor.statusline.separator (defaults to "│") +spacer Inserts a space between elements (multiple/contiguous spacers may be specified) +version-control The current branch name or detached commit hash of the opened workspace +register The current selected register +### **indent-heuristic** +Which indent heuristic to use when a new line is inserted +Defaults to `"hybrid"` +Valid options are: +* "simple" +* "tree-sitter" +* "hybrid" +### **atomic-save** +Whether to use atomic operations to write documents to disk. +This prevents data loss if the editor is interrupted while writing the file, but may +confuse some file watching/hot reloading programs. Defaults to `#true`. +### **lsp** +Blanket LSP configuration +The options are provided in a hashmap, and provided options will be merged +with the defaults. The options are as follows: + +Enables LSP +* enable: bool + +Display LSP messagess from $/progress below statusline +* display-progress-messages: bool + +Display LSP messages from window/showMessage below statusline +* display-messages: bool + +Enable automatic pop up of signature help (parameter hints) +* auto-signature-help: bool + +Display docs under signature help popup +* display-signature-help-docs: bool + +Display inlay hints +* display-inlay-hints: bool + +Maximum displayed length of inlay hints (excluding the added trailing `…`). +If it's `None`, there's no limit +* inlay-hints-length-limit: Option + +Display document color swatches +* display-color-swatches: bool + +Whether to enable snippet support +* snippets: bool + +Whether to include declaration in the goto reference query +* goto_reference_include_declaration: bool + +```scheme +(lsp (hash 'display-inlay-hints #t)) +``` + +The defaults shown from the rust side are as follows: +```rust + LspConfig { + enable: true, + display_progress_messages: false, + display_messages: true, + auto_signature_help: true, + display_signature_help_docs: true, + display_inlay_hints: false, + inlay_hints_length_limit: None, + snippets: true, + goto_reference_include_declaration: true, + display_color_swatches: true, + } + +``` +### **search** +Search configuration +Accepts two keywords, #:smart-case and #:wrap-around, both default to true. + +```scheme +(search #:smart-case #t #:wrap-around #t) +(search #:smart-case #f #:wrap-around #f) +``` +### **auto-pairs** +Automatic insertion of pairs to parentheses, brackets, +etc. Optionally, this can be a list of pairs to specify a +global list of characters to pair, or a hashmap of character to character. +Defaults to true. + +```scheme +(auto-pairs #f) +(auto-pairs #t) +(auto-pairs (list '(#\{ . #\}))) +(auto-pairs (list '(#\{ #\}))) +(auto-pairs (list (cons #\{ #\}))) +(auto-pairs (hash #\{ #\})) +``` +### **continue-comments** +Whether comments should be continued. +### **popup-border** +Set the popup border. +Valid options are: +* "none" +* "all" +* "popup" +* "menu" +### **register-lsp-notification-handler** +Register a callback to be called on LSP notifications sent from the server -> client +that aren't currently handled by Helix as a built in. + +```scheme +(register-lsp-notification-handler lsp-name event-name handler) +``` + +* lsp-name : string? +* event-name : string? +* function : (-> hash? any?) ;; Function where the first argument is the parameters + +# Examples +``` +(register-lsp-notification-handler "dart" + "dart/textDocument/publishClosingLabels" + (lambda (args) (displayln args))) +``` +### **register-lsp-call-handler** +Register a callback to be called on LSP calls sent from the server -> client +that aren't currently handled by Helix as a built in. + +```scheme +(register-lsp-call-handler lsp-name event-name handler) +``` + +* lsp-name : string? +* event-name : string? +* function : (-> hash? any?) ;; Function where the first argument is the parameters + +# Examples +``` +(register-lsp-call-handler "dart" + "dart/textDocument/publishClosingLabels" + (lambda (call-id args) (displayln args))) +``` +### **define-lsp** +Syntax: + +Registers an lsp configuration. This is a thin wrapper around passing +a hashmap manually to `set-lsp-config!`, and has a slightly more elegant +API. + +Examples: +```scheme +(define-lsp "steel-language-server" (command steel-language-server) (args '())) +(define-lsp "rust-analyzer" (config (experimental (hash 'testExplorer #t 'runnables '("cargo"))))) +(define-lsp "tinymist" (config (exportPdf "onType") (outputPath "$root/$dir/$name"))) +``` +### **cursor-shape** +Shape for cursor in each mode + +(cursor-shape #:normal (normal 'block) + #:select (select 'block) + #:insert (insert 'block)) + +# Examples + +```scheme +(cursor-shape #:normal 'block #:select 'underline #:insert 'bar) +``` +### **get-lsp-config** +Get the lsp configuration for a language server. + +Returns a hashmap which can be passed to `set-lsp-config!` +### **set-lsp-config!** +Sets the language server config for a specific language server. + +```scheme +(set-lsp-config! lsp config) +``` +* lsp : string? +* config: hash? + +This will overlay the existing configuration, much like the existing +toml definition does. + +Available options for the config hash are: +```scheme +(hash "command" "" + "args" (list "args" ...) + "environment" (hash "ENV" "VAR" ...) + "config" (hash ...) + "timeout" 100 ;; number + "required-root-patterns" (listof "pattern" ...)) + +``` + +# Examples +``` +(set-lsp-config! "jdtls" + (hash "args" (list "-data" "/home/matt/code/java-scratch/workspace"))) +``` +### **file-picker-kw** +Sets the configuration for the file picker using keywords. + +```scheme +(file-picker-kw #:hidden #t + #:follow-symlinks #t + #:deduplicate-links #t + #:parents #t + #:ignore #t + #:git-ignore #t + #:git-exclude #t + #:git-global #t + #:max-depth #f) ;; Expects either #f or an int? +``` +By default, max depth is `#f` while everything else is an int? + +To use this, call this in your `init.scm` or `helix.scm`: + +# Examples +```scheme +(file-picker-kw #:hidden #f) +``` +### **file-picker** +Sets the configuration for the file picker using var args. + +```scheme +(file-picker . args) +``` + +The args are expected to be something of the value: +```scheme +(-> FilePickerConfiguration? bool?) +``` + +These other functions in this module which follow this behavior are all +prefixed `fp-`, and include: + +* fp-hidden +* fp-follow-symlinks +* fp-deduplicate-links +* fp-parents +* fp-ignore +* fp-git-ignore +* fp-git-global +* fp-git-exclude +* fp-max-depth + +By default, max depth is `#f` while everything else is an int? + +To use this, call this in your `init.scm` or `helix.scm`: + +# Examples +```scheme +(file-picker (fp-hidden #f) (fp-parents #f)) +``` +### **soft-wrap-kw** +Sets the configuration for soft wrap using keyword args. + +```scheme +(soft-wrap-kw #:enable #f + #:max-wrap 20 + #:max-indent-retain 40 + #:wrap-indicator "↪" + #:wrap-at-text-width #f) +``` + +The options are as follows: + +* #:enable: + Soft wrap lines that exceed viewport width. Default to off +* #:max-wrap: + Maximum space left free at the end of the line. + This space is used to wrap text at word boundaries. If that is not possible within this limit + the word is simply split at the end of the line. + + This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views. + + Default to 20 +* #:max-indent-retain + Maximum number of indentation that can be carried over from the previous line when softwrapping. + If a line is indented further then this limit it is rendered at the start of the viewport instead. + + This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views. + + Default to 40 +* #:wrap-indicator + Indicator placed at the beginning of softwrapped lines + + Defaults to ↪ +* #:wrap-at-text-width + Softwrap at `text_width` instead of viewport width if it is shorter + +# Examples +```scheme +(soft-wrap-kw #:sw-enable #t) +``` +### **soft-wrap** +Sets the configuration for soft wrap using var args. + +```scheme +(soft-wrap . args) +``` + +The args are expected to be something of the value: +```scheme +(-> SoftWrapConfiguration? bool?) +``` +The options are as follows: + +* sw-enable: + Soft wrap lines that exceed viewport width. Default to off +* sw-max-wrap: + Maximum space left free at the end of the line. + This space is used to wrap text at word boundaries. If that is not possible within this limit + the word is simply split at the end of the line. + + This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views. + + Default to 20 +* sw-max-indent-retain + Maximum number of indentation that can be carried over from the previous line when softwrapping. + If a line is indented further then this limit it is rendered at the start of the viewport instead. + + This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views. + + Default to 40 +* sw-wrap-indicator + Indicator placed at the beginning of softwrapped lines + + Defaults to ↪ +* sw-wrap-at-text-width + Softwrap at `text_width` instead of viewport width if it is shorter + +# Examples +```scheme +(soft-wrap (sw-enable #t)) +``` +### **whitespace** +Sets the configuration for whitespace using var args. + +```scheme +(whitespace . args) +``` + +The args are expected to be something of the value: +```scheme +(-> WhitespaceConfiguration? bool?) +``` +The options are as follows: + +* ws-visible: + Show all visible whitespace, defaults to false +* ws-render: + manually disable or enable characters + render options (specified in hashmap): +```scheme + (hash + 'space #f + 'nbsp #f + 'nnbsp #f + 'tab #f + 'newline #f) +``` +* ws-chars: + manually set visible whitespace characters with a hashmap + character options (specified in hashmap): +```scheme + (hash + 'space #\· + 'nbsp #\⍽ + 'nnbsp #\␣ + 'tab #\→ + 'newline #\⏎ + ; Tabs will look like "→···" (depending on tab width) + 'tabpad #\·) +``` +# Examples +```scheme +(whitespace (ws-visible #t) (ws-chars (hash 'space #\·)) (ws-render (hash 'tab #f))) +``` +### **indent-guides** +Sets the configuration for indent-guides using args + +```scheme +(indent-guides . args) +``` + +The args are expected to be something of the value: +```scheme +(-> IndentGuidesConfig? bool?) +``` +The options are as follows: + +* ig-render: + Show indent guides, defaults to false +* ig-character: + character used for indent guides, defaults to "╎" +* ig-skip-levels: + amount of levels to skip, defaults to 1 + +# Examples +```scheme +(indent-guides (ig-render #t) (ig-character #\|) (ig-skip-levels 1)) +``` +### **scrolloff** +Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5. +### **scroll_lines** +Number of lines to scroll at once. Defaults to 3 +### **mouse** +Mouse support. Defaults to true. +### **shell** +Shell to use for shell commands. Defaults to ["cmd", "/C"] on Windows and ["sh", "-c"] otherwise. +### **jump-label-alphabet** +The characters that are used to generate two character jump labels. Characters at the start of the alphabet are used first. Defaults to "abcdefghijklmnopqrstuvwxyz" +### **line-number** +Line number mode. Defaults to 'absolute, set to 'relative for relative line numbers +### **cursorline** +Highlight the lines cursors are currently on. Defaults to false +### **cursorcolumn** +Highlight the columns cursors are currently on. Defaults to false +### **middle-click-paste** +Middle click paste support. Defaults to true +### **auto-completion** +Automatic auto-completion, automatically pop up without user trigger. Defaults to true. +### **auto-format** +Automatic formatting on save. Defaults to true. +### **auto-save** +Automatic save on focus lost and/or after delay. +Time delay in milliseconds since last edit after which auto save timer triggers. +Time delay defaults to false with 3000ms delay. Focus lost defaults to false. + +### **text-width** +Set a global text_width +### **idle-timeout** +Time in milliseconds since last keypress before idle timers trigger. +Used for various UI timeouts. Defaults to 250ms. +### **completion-timeout** + +Time in milliseconds after typing a word character before auto completions +are shown, set to 5 for instant. Defaults to 250ms. + +### **preview-completion-insert** +Whether to insert the completion suggestion on hover. Defaults to true. +### **completion-trigger-len** +Length to trigger completions +### **completion-replace** +Whether to instruct the LSP to replace the entire word when applying a completion +or to only insert new text +### **auto-info** +Whether to display infoboxes. Defaults to true. +### **true-color** +Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`. +### **insert-final-newline** +Whether to automatically insert a trailing line-ending on write if missing. Defaults to `true` +### **color-modes** +Whether to color modes with different colors. Defaults to `false`. +### **gutters** +Gutter configuration +### **undercurl** +Set to `true` to override automatic detection of terminal undercurl support in the event of a false negative. Defaults to `false`. +### **terminal** +Terminal config +### **rulers** +Column numbers at which to draw the rulers. Defaults to `[]`, meaning no rulers +### **bufferline** +Persistently display open buffers along the top +### **workspace-lsp-roots** +Workspace specific lsp ceiling dirs +### **default-line-ending** +Which line ending to choose for new documents. Defaults to `native`. i.e. `crlf` on Windows, otherwise `lf`. +### **smart-tab** +Enables smart tab +### **rainbow-brackets** +Enabled rainbow brackets +### **keybindings** +Keybindings config +### **set-keybindings!** +Override the global keybindings with the provided keymap +### **inline-diagnostics-cursor-line-enable** +Inline diagnostics cursor line +### **inline-diagnostics-other-lines-enable** +Inline diagnostics other lines +### **inline-diagnostics-end-of-line-enable** +Inline diagnostics end of line +### **inline-diagnostics-min-diagnostics-width** +Inline diagnostics min diagnostics width +### **inline-diagnostics-prefix-len** +Inline diagnostics prefix length +### **inline-diagnostics-max-wrap** +Inline diagnostics maximum wrap +### **inline-diagnostics-max-diagnostics** +Inline diagnostics max diagnostics +### **get-language-config** +Get the configuration for a specific language +### **set-language-config!** +Set the language configuration +# /home/matt/.steel/cogs/helix/commands.scm +### **quit** +Close the current view. +### **quit!** +Force close the current view, ignoring unsaved changes. +### **open** +Open a file from disk into the current view. +### **buffer-close** +Close the current buffer. +### **buffer-close!** +Close the current buffer forcefully, ignoring unsaved changes. +### **buffer-close-others** +Close all buffers but the currently focused one. +### **buffer-close-others!** +Force close all buffers but the currently focused one. +### **buffer-close-all** +Close all buffers without quitting. +### **buffer-close-all!** +Force close all buffers ignoring unsaved changes without quitting. +### **buffer-next** +Goto next buffer. +### **buffer-previous** +Goto previous buffer. +### **write** +Write changes to disk. Accepts an optional path (:write some/path.txt) +### **write!** +Force write changes to disk creating necessary subdirectories. Accepts an optional path (:write! some/path.txt) +### **write-buffer-close** +Write changes to disk and closes the buffer. Accepts an optional path (:write-buffer-close some/path.txt) +### **write-buffer-close!** +Force write changes to disk creating necessary subdirectories and closes the buffer. Accepts an optional path (:write-buffer-close! some/path.txt) +### **new** +Create a new scratch buffer. +### **format** +Format the file using an external formatter or language server. +### **indent-style** +Set the indentation style for editing. ('t' for tabs or 1-16 for number of spaces.) +### **line-ending** +Set the document's default line ending. Options: crlf, lf. +### **earlier** +Jump back to an earlier point in edit history. Accepts a number of steps or a time span. +### **later** +Jump to a later point in edit history. Accepts a number of steps or a time span. +### **write-quit** +Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt) +### **write-quit!** +Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt) +### **write-all** +Write changes from all buffers to disk. +### **write-all!** +Forcefully write changes from all buffers to disk creating necessary subdirectories. +### **write-quit-all** +Write changes from all buffers to disk and close all views. +### **write-quit-all!** +Write changes from all buffers to disk and close all views forcefully (ignoring unsaved changes). +### **quit-all** +Close all views. +### **quit-all!** +Force close all views ignoring unsaved changes. +### **cquit** +Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2). +### **cquit!** +Force quit with exit code (default 1) ignoring unsaved changes. Accepts an optional integer exit code (:cq! 2). +### **theme** +Change the editor theme (show current theme if no name specified). +### **yank-join** +Yank joined selections. A separator can be provided as first argument. Default value is newline. +### **clipboard-yank** +Yank main selection into system clipboard. +### **clipboard-yank-join** +Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline. +### **primary-clipboard-yank** +Yank main selection into system primary clipboard. +### **primary-clipboard-yank-join** +Yank joined selections into system primary clipboard. A separator can be provided as first argument. Default value is newline. +### **clipboard-paste-after** +Paste system clipboard after selections. +### **clipboard-paste-before** +Paste system clipboard before selections. +### **clipboard-paste-replace** +Replace selections with content of system clipboard. +### **primary-clipboard-paste-after** +Paste primary clipboard after selections. +### **primary-clipboard-paste-before** +Paste primary clipboard before selections. +### **primary-clipboard-paste-replace** +Replace selections with content of system primary clipboard. +### **show-clipboard-provider** +Show clipboard provider name in status bar. +### **change-current-directory** +Change the current working directory. +### **show-directory** +Show the current working directory. +### **encoding** +Set encoding. Based on `https://encoding.spec.whatwg.org`. +### **character-info** +Get info about the character under the primary cursor. +### **reload** +Discard changes and reload from the source file. +### **reload-all** +Discard changes and reload all documents from the source files. +### **update** +Write changes only if the file has been modified. +### **lsp-workspace-command** +Open workspace command picker +### **lsp-restart** +Restarts the given language servers, or all language servers that are used by the current file if no arguments are supplied +### **lsp-stop** +Stops the given language servers, or all language servers that are used by the current file if no arguments are supplied +### **tree-sitter-scopes** +Display tree sitter scopes, primarily for theming and development. +### **tree-sitter-highlight-name** +Display name of tree-sitter highlight scope under the cursor. +### **debug-start** +Start a debug session from a given template with given parameters. +### **debug-remote** +Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters. +### **debug-eval** +Evaluate expression in current debug context. +### **vsplit** +Open the file in a vertical split. +### **vsplit-new** +Open a scratch buffer in a vertical split. +### **hsplit** +Open the file in a horizontal split. +### **hsplit-new** +Open a scratch buffer in a horizontal split. +### **tutor** +Open the tutorial. +### **goto** +Goto line number. +### **set-language** +Set the language of current buffer (show current language if no value specified). +### **set-option** +Set a config option at runtime. +For example to disable smart case search, use `:set search.smart-case false`. +### **toggle-option** +Toggle a config option at runtime. +For example to toggle smart case search, use `:toggle search.smart-case`. +### **get-option** +Get the current value of a config option. +### **sort** +Sort ranges in selection. +### **reflow** +Hard-wrap the current selection of lines to a given width. +### **tree-sitter-subtree** +Display the smallest tree-sitter subtree that spans the primary selection, primarily for debugging queries. +### **config-reload** +Refresh user config. +### **config-open** +Open the user config.toml file. +### **config-open-workspace** +Open the workspace config.toml file. +### **log-open** +Open the helix log file. +### **insert-output** +Run shell command, inserting output before each selection. +### **append-output** +Run shell command, appending output after each selection. +### **pipe** +Pipe each selection to the shell command. +### **pipe-to** +Pipe each selection to the shell command, ignoring output. +### **run-shell-command** +Run a shell command +### **reset-diff-change** +Reset the diff change at the cursor position. +### **clear-register** +Clear given register. If no argument is provided, clear all registers. +### **redraw** +Clear and re-render the whole UI +### **move** +Move the current buffer and its corresponding file to a different path +### **yank-diagnostic** +Yank diagnostic(s) under primary cursor to register, or clipboard by default +### **read** +Load a file into buffer +### **echo** +Prints the given arguments to the statusline. +### **noop** +Does nothing. +### **goto-column** +Move the cursor to the given character index within the same line +### **goto-line** +Move the cursor to the given line +# /home/matt/.steel/cogs/helix/misc.scm +### **hx.cx->pos** +DEPRECATED: Please use `cursor-position` +### **cursor-position** +Returns the cursor position within the current buffer as an integer +### **get-active-lsp-clients** +Get all language servers, that are attached to the current buffer +### **mode-switch-old** +Return the old mode from the event payload +### **mode-switch-new** +Return the new mode from the event payload +### **lsp-client-initialized?** +Return if the lsp client is initialized +### **lsp-client-name** +Get the name of the lsp client +### **lsp-client-offset-encoding** +Get the offset encoding of the lsp client +### **hx.custom-insert-newline** +DEPRECATED: Please use `insert-newline-hook` +### **insert-newline-hook** +Inserts a new line with the provided indentation. + +```scheme +(insert-newline-hook indent-string) +``` + +indent-string : string? + +### **push-component!** + +Push a component on to the top of the stack. + +```scheme +(push-component! component) +``` + +component : WrappedDynComponent? + +### **pop-last-component!** +DEPRECATED: Please use `pop-last-component-by-name!` +### **pop-last-component-by-name!** +Pops the last component off of the stack by name. In other words, +it removes the component matching this name from the stack. + +```scheme +(pop-last-component-by-name! name) +``` + +name : string? + +### **enqueue-thread-local-callback** + +Enqueue a function to be run following this context of execution. This could +be useful for yielding back to the editor in the event you want updates to happen +before your function is run. + +```scheme +(enqueue-thread-local-callback callback) +``` + +callback : (-> any?) + Function with no arguments. + +# Examples + +```scheme +(enqueue-thread-local-callback (lambda () (theme "focus_nova"))) +``` + +### **set-status!** +Sets the content of the status line, with the info severity +### **set-warning!** +Sets the content of the status line, with the warning severity +### **set-error!** +Sets the content of the status line, with the error severity +### **send-lsp-command** +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)))) +``` +### **send-lsp-notification** +Send an LSP notification. 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. Unlike +`send-lsp-command`, this does not expect a response and is used for +fire-and-forget notifications. + +# Example +```scheme +(send-lsp-notification "copilot" + "textDocument/didShowCompletion" + (hash "item" + (hash "insertText" "a helpful suggestion" + "range" (hash "start" (hash "line" 1 "character" 0) + "end" (hash "line" 1 "character" 2))))) +``` +### **lsp-reply-ok** +Send a successful reply to an LSP request with the given result. + +```scheme +(lsp-reply-ok lsp-name request-id result) +``` + +* lsp-name : string? - Name of the language server +* request-id : string? - ID of the request to respond to +* result : any? - The result value to send back + +# Examples +```scheme +;; Reply to a request with id "123" from rust-analyzer +(lsp-reply-ok "rust-analyzer" "123" (hash "result" "value")) +``` +### **acquire-context-lock** + +Schedule a function to run on the main thread. This is a fairly low level function, and odds are +you'll want to use some abstractions on top of this. + +The provided function will get enqueued to run on the main thread, and during the duration of the functions +execution, the provided mutex will be locked. + +```scheme +(acquire-context-lock callback-fn mutex) +``` + +callback-fn : (-> void?) + Function with no arguments + +mutex : mutex? +### **enqueue-thread-local-callback-with-delay** + +Enqueue a function to be run following this context of execution, after a delay. This could +be useful for yielding back to the editor in the event you want updates to happen +before your function is run. + +```scheme +(enqueue-thread-local-callback-with-delay delay callback) +``` + +delay : int? + Time to delay the callback by in milliseconds + +callback : (-> any?) + Function with no arguments. + +# Examples + +```scheme +(enqueue-thread-local-callback-with-delay 1000 (lambda () (theme "focus_nova"))) ;; Run after 1 second +`` + +### **helix-await-callback** +DEPRECATED: Please use `await-callback` +### **await-callback** + +Await the given value, and call the callback function on once the future is completed. + +```scheme +(await-callback future callback) +``` + +* future : future? +* callback (-> any?) + Function with no arguments +### **add-inlay-hint** + +Warning: this is experimental + +Adds an inlay hint at the given character index. Returns the (first-line, last-line) list +associated with this snapshot of the inlay hints. Use this pair of line numbers to invalidate +the inlay hints. + +```scheme +(add-inlay-hint char-index completion) -> (list int? int?) +``` + +char-index : int? +completion : string? + +### **remove-inlay-hint** + +Warning: this is experimental and should not be used. +This will most likely be removed soon. + +Removes an inlay hint at the given character index. Note - to remove +properly, the completion must match what was already there. + +```scheme +(remove-inlay-hint char-index completion) +``` + +char-index : int? +completion : string? + +### **remove-inlay-hint-by-id** + +Warning: this is experimental + +Removes an inlay hint by the id that was associated with the added inlay hints. + +```scheme +(remove-inlay-hint first-line last-line) +``` + +first-line : int? +last-line : int? + +# /home/matt/.steel/cogs/helix/editor.scm +### **editor-focus** + +Get the current focus of the editor, as a `ViewId`. + +```scheme +(editor-focus) -> ViewId +``` + +### **editor-mode** + +Get the current mode of the editor + +```scheme +(editor-mode) -> Mode? +``` + +### **string->editor-mode** + +Create an editor mode from a string, or false if it string was not one of +"normal", "insert", or "select" + +```scheme +(string->editor-mode "normal") -> (or Mode? #f) +``` + +### **cx->themes** +DEPRECATED: Please use `themes->list` +### **themes->list** + +Get the current themes as a list of strings. + +```scheme +(themes->list) -> (listof string?) +``` + +### **editor-all-documents** + +Get a list of all of the document ids that are currently open. + +```scheme +(editor-all-documents) -> (listof DocumentId?) +``` + +### **cx->cursor** +DEPRECATED: Please use `current-cursor` +### **current-cursor** +Gets the primary cursor position in screen coordinates, +or `#false` if the primary cursor is not visible on screen. + +```scheme +(current-cursor) -> (listof? (or Position? #false) CursorKind) +``` + +### **editor-focused-buffer-area** + +Get the `Rect` associated with the currently focused buffer. + +```scheme +(editor-focused-buffer-area) -> (or Rect? #false) +``` + +### **selected-register!** +Get currently selected register +### **editor->doc-id** +Get the document from a given view. +### **editor-switch!** +Open the document in a vertical split. +### **editor-set-focus!** +Set focus on the view. +### **editor-set-mode!** +Set the editor mode. +### **editor-doc-in-view?** +Check whether the current view contains a document. +### **set-scratch-buffer-name!** +Set the name of a scratch buffer. +### **set-buffer-uri!** +Set the URI of the buffer +### **editor-doc-exists?** +Check if a document exists. +### **editor-document-last-saved** +Check when a document was last saved (returns a `SystemTime`) +### **editor-document->language** +Get the language for the document +### **editor-document-dirty?** +Check if a document has unsaved changes +### **editor-document-reload** +Reload a document. +### **editor->text** +Get the document as a rope. +### **editor-document->path** +Get the path to a document. +### **register->value** +Get register value as a list of strings. +### **set-editor-clip-top!** +Set the editor clipping at the top. +### **set-editor-clip-right!** +Set the editor clipping at the right. +### **set-editor-clip-left!** +Set the editor clipping at the left. +### **set-editor-clip-bottom!** +Set the editor clipping at the bottom. +# /home/matt/.steel/cogs/helix/themes.scm +### **register-theme** +Register this theme with helix for use +### **attribute** +Class attributes, HTML tag attributes +### **type** +Types +### **type.builtin** +Primitive types provided by the language (`int`, `usize`) +### **type.parameter** +Generic type parameters (`T`) +### **type.enum** +Enum usage +### **type.enum.variant** +Enum variant +### **constructor** +Constructor usage +### **constant** +Constants usage +### **constant.builtin** +Special constants provided by the language (`true`, `false`, `nil`, etc) +### **constant.builtin.boolean** +A special case for highlighting individual booleans +### **constant.character** +Character usage +### **constant.character.escape** +Highlighting individual escape characters +### **constant.numeric** +Numbers +### **constant.numeric.integer** +Integers +### **constant.numeric.float** +Floats +### **string** +Highlighting strings +### **string.regexp** +Highlighting regular expressions +### **string.special** +Special strings +### **string.special.path** +Highlighting paths +### **string.special.url** +Highlighting URLs +### **string.special.symbol** +Erlang/Elixir atoms, Ruby symbols, Clojure keywords +### **comment** +Highlighting comments +### **comment.line** +Single line comments (`//`) +### **comment.block** +Block comments (`/* */`) +### **comment.block.documentation** +Documentation comments (e.g. `///` in Rust) +### **variable** +Variables +### **variable.builtin** +Reserved language variables (`self`, `this`, `super`, etc.) +### **variable.parameter** +Function parameters +### **variable.other** +Other variables +### **variable.other.member** +Fields of composite data types (e.g. structs, unions) +### **variable.other.member.private** +Private fields that use a unique syntax (currently just EMCAScript-based languages) +### **label** +Highlighting labels +### **punctuation** +Highlighting punctuation +### **punctuation.delimiter** +Commas, colon +### **punctuation.bracket** +Parentheses, angle brackets, etc. +### **punctuation.special** +String interpolation brackets +### **keyword** +Highlighting keywords +### **keyword.control** +Control keywords +### **keyword.control.conditional** +if, else +### **keyword.control.repeat** +for, while, loop +### **keyword.control.import** +import, export +### **keyword.control.return** +return keyword +### **keyword.control.exception** +exception keyword +### **keyword.operator** +or, in +### **keyword.directive** +Preprocessor directives (`#if` in C) +### **keyword.function** +fn, func +### **keyword.storage** +Keywords describing how things are stored +### **keyword.storage.type** +The type of something, `class`, `function`, `var`, `let`, etc +### **keyword.storage.modifier** +Storage modifiers like `static`, `mut`, `const`, `ref`, etc +### **operator** +Operators such as `||`, `+=`, `>`, etc +### **function** +Highlighting function calls +### **function.builtin** +Builtin functions +### **function.method** +Calling methods +### **function.method.private** +Private methods that use a unique syntax (currently just ECMAScript-based languages) +### **function.macro** +Highlighting macros +### **function.special** +Preprocessor in C +### **tag** +Tags (e.g. in HTML) +### **tag.builtin** +Builtin tags +### **markup** +Highlighting markdown +### **markup.heading** +Markdown heading +### **markup.heading.marker** +Markdown heading marker +### **markup.heading.marker.1** +Markdown heading text h1 +### **markup.heading.marker.2** +Markdown heading text h2 +### **markup.heading.marker.3** +Markdown heading text h3 +### **markup.heading.marker.4** +Markdown heading text h4 +### **markup.heading.marker.5** +Markdown heading text h5 +### **markup.heading.marker.6** +Markdown heading text h6 +### **markup.list** +Markdown lists +### **markup.list.unnumbered** +Unnumbered markdown lists +### **markup.list.numbered** +Numbered markdown lists +### **markup.list.checked** +Checked markdown lists +### **markup.list.unchecked** +Unchecked markdown lists +### **markup.bold** +Markdown bold +### **markup.italic** +Markdown italics +### **markup.strikethrough** +Markdown strikethrough +### **markup.link** +Markdown links +### **markup.link.url** +URLs pointed to by links +### **markup.link.label** +non-URL link references +### **markup.link.text** +URL and image descriptions in links +### **markup.quote** +Markdown quotes +### **markup.raw** +Markdown raw +### **markup.raw.inline** +Markdown inline raw +### **markup.raw.block** +Markdown raw block +### **diff** +Version control changes +### **diff.plus** +Version control additions +### **diff.plus.gutter** +Version control addition gutter indicator +### **diff.minus** +Version control deletions +### **diff.minus.gutter** +Version control deletion gutter indicator +### **diff.delta** +Version control modifications +### **diff.delta.moved** +Renamed or moved files/changes +### **diff.delta.conflict** +Merge conflicts +### **diff.delta.gutter** +Gutter indicator +### **markup.normal.completion** +For completion doc popup UI +### **markup.normal.hover** +For hover popup UI +### **markup.heading.completion** +For completion doc popup UI +### **markup.heading.hover** +For hover popup UI +### **markup.raw.inline.completion** +For completion doc popup UI +### **markup.raw.inline.hover** +For hover popup UI +### **ui.background.separator** +Picker separator below input line +### **ui.cursor.match** +Matching bracket etc. +### **ui.cursor.primary** +Cursor with primary selection +### **ui.debug.breakpoint** +Breakpoint indicator, found in the gutter +### **ui.debug.active** +Indicator for the line at which debugging execution is paused at, found in the gutter +### **ui.gutter** +Gutter +### **ui.gutter.selected** +Gutter for the line the cursor is on +### **ui.highlight.frameline** +Line at which debugging execution is paused at +### **ui.linenr** +Line numbers +### **ui.linenr.selected** +Line number for the line the cursor is on +### **ui.statusline** +Statusline +### **ui.statusline.inactive** +Statusline (unfocused document) +### **ui.statusline.normal** +Statusline mode during normal mode (only if editor.color-modes is enabled) +### **ui.statusline.insert** +Statusline mode during insert mode (only if editor.color-modes is enabled) +### **ui.statusline.select** +Statusline mode during select mode (only if editor.color-modes is enabled) +### **ui.statusline.separator** +Separator character in statusline +### **ui.bufferline** +Style for the buffer line +### **ui.bufferline.active** +Style for the active buffer in buffer line +### **ui.bufferline.background** +Style for the bufferline background +### **ui.popup** +Documentation popups (e.g. Space + k) +### **ui.popup.info** +Prompt for multiple key options +### **ui.window** +Borderline separating splits +### **ui.help** +Description box for commands +### **ui.text** +Default text style, command prompts, popup text, etc. +### **ui.text.focus** +The currently selected line in the picker +### **ui.text.inactive** +Same as ui.text but when the text is inactive (e.g. suggestions) +### **ui.text.info** +The key: command text in ui.popup.info boxes +### **ui.virtual.ruler** +Ruler columns (see the editor.rules config) +### **ui.virtual.whitespace** +Visible whitespace characters +### **ui.virtual.indent-guide** +Vertical indent width guides +### **ui.virtual.inlay-hint** +Default style for inlay hints of all kinds +### **ui.virtual.inlay-hint.parameter** +Style for inlay hints of kind `parameter` (LSPs are not rquired to set a kind) +### **ui.virtual.inlay-hint.type** +Style for inlay hints of kind `type` (LSPs are not required to set a kind) +### **ui.virtual.wrap** +Soft-wrap indicator (see the editor.soft-wrap config) +### **ui.virtual.jump-label** +Style for virtual jump labels +### **ui.menu** +Code and command completion menus +### **ui.menu.selected** +Selected autocomplete item +### **ui.menu.scroll** +fg sets thumb color, bg sets track color of scrollbar +### **ui.selection** +For selections in the editing area +### **ui.highlight** +Highlighted lines in the picker preview +### **ui.cursorline** +The line of the cursor (if cursorline is enabled) +### **ui.cursorline.primary** +The line of the primary cursor (if cursorline is enabled) +### **ui.cursorline.secondary** +The line of the secondary cursor (if cursorline is enabled) +### **ui.cursorcolumn.primary** +The column of the primary cursor (if cursorcolumn is enabled) +### **ui.cursorcolumn.secondary** +The column of the secondary cursor (if cursorcolumn is enabled) +### **warning** +Diagnostics warning (gutter) +### **error** +Diagnostics error (gutter) +### **info** +Diagnostics info (gutter) +### **hint** +Diagnostics hint (gutter) +### **diagnostic** +Diagnostics fallback style (editing area) +### **diagnostic.hint** +Diagnostics hint (editing area) +### **diagnostic.info** +Diagnostics info (editing area) +### **diagnostic.warning** +Diagnostics warning (editing area) +### **diagnostic.error** +Diagnostics error (editing area) +### **diagnostic.unnecessary** +Diagnostics with unnecessary tag (editing area) +### **diagnostic.deprecated** +Diagnostics with deprecated tag (editing area) +# /home/matt/.steel/cogs/helix/static.scm +### **no_op** +Do nothing +### **move_char_left** +Move left +### **move_char_right** +Move right +### **move_line_up** +Move up +### **move_line_down** +Move down +### **move_visual_line_up** +Move up +### **move_visual_line_down** +Move down +### **extend_char_left** +Extend left +### **extend_char_right** +Extend right +### **extend_line_up** +Extend up +### **extend_line_down** +Extend down +### **extend_visual_line_up** +Extend up +### **extend_visual_line_down** +Extend down +### **copy_selection_on_next_line** +Copy selection on next line +### **copy_selection_on_prev_line** +Copy selection on previous line +### **move_next_word_start** +Move to start of next word +### **move_prev_word_start** +Move to start of previous word +### **move_next_word_end** +Move to end of next word +### **move_prev_word_end** +Move to end of previous word +### **move_next_long_word_start** +Move to start of next long word +### **move_prev_long_word_start** +Move to start of previous long word +### **move_next_long_word_end** +Move to end of next long word +### **move_prev_long_word_end** +Move to end of previous long word +### **move_next_sub_word_start** +Move to start of next sub word +### **move_prev_sub_word_start** +Move to start of previous sub word +### **move_next_sub_word_end** +Move to end of next sub word +### **move_prev_sub_word_end** +Move to end of previous sub word +### **move_parent_node_end** +Move to end of the parent node +### **move_parent_node_start** +Move to beginning of the parent node +### **extend_next_word_start** +Extend to start of next word +### **extend_prev_word_start** +Extend to start of previous word +### **extend_next_word_end** +Extend to end of next word +### **extend_prev_word_end** +Extend to end of previous word +### **extend_next_long_word_start** +Extend to start of next long word +### **extend_prev_long_word_start** +Extend to start of previous long word +### **extend_next_long_word_end** +Extend to end of next long word +### **extend_prev_long_word_end** +Extend to end of prev long word +### **extend_next_sub_word_start** +Extend to start of next sub word +### **extend_prev_sub_word_start** +Extend to start of previous sub word +### **extend_next_sub_word_end** +Extend to end of next sub word +### **extend_prev_sub_word_end** +Extend to end of prev sub word +### **extend_parent_node_end** +Extend to end of the parent node +### **extend_parent_node_start** +Extend to beginning of the parent node +### **find_till_char** +Move till next occurrence of char +### **find_next_char** +Move to next occurrence of char +### **extend_till_char** +Extend till next occurrence of char +### **extend_next_char** +Extend to next occurrence of char +### **till_prev_char** +Move till previous occurrence of char +### **find_prev_char** +Move to previous occurrence of char +### **extend_till_prev_char** +Extend till previous occurrence of char +### **extend_prev_char** +Extend to previous occurrence of char +### **repeat_last_motion** +Repeat last motion +### **replace** +Replace with new char +### **switch_case** +Switch (toggle) case +### **switch_to_uppercase** +Switch to uppercase +### **switch_to_lowercase** +Switch to lowercase +### **page_up** +Move page up +### **page_down** +Move page down +### **half_page_up** +Move half page up +### **half_page_down** +Move half page down +### **page_cursor_up** +Move page and cursor up +### **page_cursor_down** +Move page and cursor down +### **page_cursor_half_up** +Move page and cursor half up +### **page_cursor_half_down** +Move page and cursor half down +### **select_all** +Select whole document +### **select_regex** +Select all regex matches inside selections +### **split_selection** +Split selections on regex matches +### **split_selection_on_newline** +Split selection on newlines +### **merge_selections** +Merge selections +### **merge_consecutive_selections** +Merge consecutive selections +### **search** +Search for regex pattern +### **rsearch** +Reverse search for regex pattern +### **search_next** +Select next search match +### **search_prev** +Select previous search match +### **extend_search_next** +Add next search match to selection +### **extend_search_prev** +Add previous search match to selection +### **search_selection** +Use current selection as search pattern +### **search_selection_detect_word_boundaries** +Use current selection as the search pattern, automatically wrapping with `\b` on word boundaries +### **make_search_word_bounded** +Modify current search to make it word bounded +### **global_search** +Global search in workspace folder +### **extend_line** +Select current line, if already selected, extend to another line based on the anchor +### **extend_line_below** +Select current line, if already selected, extend to next line +### **extend_line_above** +Select current line, if already selected, extend to previous line +### **select_line_above** +Select current line, if already selected, extend or shrink line above based on the anchor +### **select_line_below** +Select current line, if already selected, extend or shrink line below based on the anchor +### **extend_to_line_bounds** +Extend selection to line bounds +### **shrink_to_line_bounds** +Shrink selection to line bounds +### **delete_selection** +Delete selection +### **delete_selection_noyank** +Delete selection without yanking +### **change_selection** +Change selection +### **change_selection_noyank** +Change selection without yanking +### **collapse_selection** +Collapse selection into single cursor +### **flip_selections** +Flip selection cursor and anchor +### **ensure_selections_forward** +Ensure all selections face forward +### **insert_mode** +Insert before selection +### **append_mode** +Append after selection +### **command_mode** +Enter command mode +### **file_picker** +Open file picker +### **file_picker_in_current_buffer_directory** +Open file picker at current buffer's directory +### **file_picker_in_current_directory** +Open file picker at current working directory +### **file_explorer** +Open file explorer in workspace root +### **file_explorer_in_current_buffer_directory** +Open file explorer at current buffer's directory +### **file_explorer_in_current_directory** +Open file explorer at current working directory +### **code_action** +Perform code action +### **buffer_picker** +Open buffer picker +### **jumplist_picker** +Open jumplist picker +### **symbol_picker** +Open symbol picker +### **syntax_symbol_picker** +Open symbol picker from syntax information +### **lsp_or_syntax_symbol_picker** +Open symbol picker from LSP or syntax information +### **changed_file_picker** +Open changed file picker +### **select_references_to_symbol_under_cursor** +Select symbol references +### **workspace_symbol_picker** +Open workspace symbol picker +### **syntax_workspace_symbol_picker** +Open workspace symbol picker from syntax information +### **lsp_or_syntax_workspace_symbol_picker** +Open workspace symbol picker from LSP or syntax information +### **diagnostics_picker** +Open diagnostic picker +### **workspace_diagnostics_picker** +Open workspace diagnostic picker +### **last_picker** +Open last picker +### **insert_at_line_start** +Insert at start of line +### **insert_at_line_end** +Insert at end of line +### **open_below** +Open new line below selection +### **open_above** +Open new line above selection +### **normal_mode** +Enter normal mode +### **select_mode** +Enter selection extend mode +### **exit_select_mode** +Exit selection mode +### **goto_definition** +Goto definition +### **goto_declaration** +Goto declaration +### **add_newline_above** +Add newline above +### **add_newline_below** +Add newline below +### **goto_type_definition** +Goto type definition +### **goto_implementation** +Goto implementation +### **goto_file_start** +Goto line number else file start +### **goto_file_end** +Goto file end +### **extend_to_file_start** +Extend to line number else file start +### **extend_to_file_end** +Extend to file end +### **goto_file** +Goto files/URLs in selections +### **goto_file_hsplit** +Goto files in selections (hsplit) +### **goto_file_vsplit** +Goto files in selections (vsplit) +### **goto_reference** +Goto references +### **goto_window_top** +Goto window top +### **goto_window_center** +Goto window center +### **goto_window_bottom** +Goto window bottom +### **goto_last_accessed_file** +Goto last accessed file +### **goto_last_modified_file** +Goto last modified file +### **goto_last_modification** +Goto last modification +### **goto_line** +Goto line +### **goto_last_line** +Goto last line +### **extend_to_last_line** +Extend to last line +### **goto_first_diag** +Goto first diagnostic +### **goto_last_diag** +Goto last diagnostic +### **goto_next_diag** +Goto next diagnostic +### **goto_prev_diag** +Goto previous diagnostic +### **goto_next_change** +Goto next change +### **goto_prev_change** +Goto previous change +### **goto_first_change** +Goto first change +### **goto_last_change** +Goto last change +### **goto_line_start** +Goto line start +### **goto_line_end** +Goto line end +### **goto_column** +Goto column +### **extend_to_column** +Extend to column +### **goto_next_buffer** +Goto next buffer +### **goto_previous_buffer** +Goto previous buffer +### **goto_line_end_newline** +Goto newline at line end +### **goto_first_nonwhitespace** +Goto first non-blank in line +### **trim_selections** +Trim whitespace from selections +### **extend_to_line_start** +Extend to line start +### **extend_to_first_nonwhitespace** +Extend to first non-blank in line +### **extend_to_line_end** +Extend to line end +### **extend_to_line_end_newline** +Extend to line end +### **signature_help** +Show signature help +### **smart_tab** +Insert tab if all cursors have all whitespace to their left; otherwise, run a separate command. +### **insert_tab** +Insert tab char +### **insert_newline** +Insert newline char +### **insert_char_interactive** +Insert an interactively-chosen char +### **append_char_interactive** +Append an interactively-chosen char +### **delete_char_backward** +Delete previous char +### **delete_char_forward** +Delete next char +### **delete_word_backward** +Delete previous word +### **delete_word_forward** +Delete next word +### **kill_to_line_start** +Delete till start of line +### **kill_to_line_end** +Delete till end of line +### **undo** +Undo change +### **redo** +Redo change +### **earlier** +Move backward in history +### **later** +Move forward in history +### **commit_undo_checkpoint** +Commit changes to new checkpoint +### **yank** +Yank selection +### **yank_to_clipboard** +Yank selections to clipboard +### **yank_to_primary_clipboard** +Yank selections to primary clipboard +### **yank_joined** +Join and yank selections +### **yank_joined_to_clipboard** +Join and yank selections to clipboard +### **yank_main_selection_to_clipboard** +Yank main selection to clipboard +### **yank_joined_to_primary_clipboard** +Join and yank selections to primary clipboard +### **yank_main_selection_to_primary_clipboard** +Yank main selection to primary clipboard +### **replace_with_yanked** +Replace with yanked text +### **replace_selections_with_clipboard** +Replace selections by clipboard content +### **replace_selections_with_primary_clipboard** +Replace selections by primary clipboard +### **paste_after** +Paste after selection +### **paste_before** +Paste before selection +### **paste_clipboard_after** +Paste clipboard after selections +### **paste_clipboard_before** +Paste clipboard before selections +### **paste_primary_clipboard_after** +Paste primary clipboard after selections +### **paste_primary_clipboard_before** +Paste primary clipboard before selections +### **indent** +Indent selection +### **unindent** +Unindent selection +### **format_selections** +Format selection +### **join_selections** +Join lines inside selection +### **join_selections_space** +Join lines inside selection and select spaces +### **keep_selections** +Keep selections matching regex +### **remove_selections** +Remove selections matching regex +### **align_selections** +Align selections in column +### **keep_primary_selection** +Keep primary selection +### **remove_primary_selection** +Remove primary selection +### **completion** +Invoke completion popup +### **hover** +Show docs for item under cursor +### **toggle_comments** +Comment/uncomment selections +### **toggle_line_comments** +Line comment/uncomment selections +### **toggle_block_comments** +Block comment/uncomment selections +### **rotate_selections_forward** +Rotate selections forward +### **rotate_selections_backward** +Rotate selections backward +### **rotate_selection_contents_forward** +Rotate selection contents forward +### **rotate_selection_contents_backward** +Rotate selections contents backward +### **reverse_selection_contents** +Reverse selections contents +### **expand_selection** +Expand selection to parent syntax node +### **shrink_selection** +Shrink selection to previously expanded syntax node +### **select_next_sibling** +Select next sibling in the syntax tree +### **select_prev_sibling** +Select previous sibling the in syntax tree +### **select_all_siblings** +Select all siblings of the current node +### **select_all_children** +Select all children of the current node +### **jump_forward** +Jump forward on jumplist +### **jump_backward** +Jump backward on jumplist +### **save_selection** +Save current selection to jumplist +### **jump_view_right** +Jump to right split +### **jump_view_left** +Jump to left split +### **jump_view_up** +Jump to split above +### **jump_view_down** +Jump to split below +### **swap_view_right** +Swap with right split +### **swap_view_left** +Swap with left split +### **swap_view_up** +Swap with split above +### **swap_view_down** +Swap with split below +### **transpose_view** +Transpose splits +### **rotate_view** +Goto next window +### **rotate_view_reverse** +Goto previous window +### **hsplit** +Horizontal bottom split +### **hsplit_new** +Horizontal bottom split scratch buffer +### **vsplit** +Vertical right split +### **vsplit_new** +Vertical right split scratch buffer +### **wclose** +Close window +### **wonly** +Close windows except current +### **select_register** +Select register +### **insert_register** +Insert register +### **copy_between_registers** +Copy between two registers +### **align_view_middle** +Align view middle +### **align_view_top** +Align view top +### **align_view_center** +Align view center +### **align_view_bottom** +Align view bottom +### **scroll_up** +Scroll view up +### **scroll_down** +Scroll view down +### **match_brackets** +Goto matching bracket +### **surround_add** +Surround add +### **surround_replace** +Surround replace +### **surround_delete** +Surround delete +### **select_textobject_around** +Select around object +### **select_textobject_inner** +Select inside object +### **goto_next_function** +Goto next function +### **goto_prev_function** +Goto previous function +### **goto_next_class** +Goto next type definition +### **goto_prev_class** +Goto previous type definition +### **goto_next_parameter** +Goto next parameter +### **goto_prev_parameter** +Goto previous parameter +### **goto_next_comment** +Goto next comment +### **goto_prev_comment** +Goto previous comment +### **goto_next_test** +Goto next test +### **goto_prev_test** +Goto previous test +### **goto_next_xml_element** +Goto next (X)HTML element +### **goto_prev_xml_element** +Goto previous (X)HTML element +### **goto_next_entry** +Goto next pairing +### **goto_prev_entry** +Goto previous pairing +### **goto_next_paragraph** +Goto next paragraph +### **goto_prev_paragraph** +Goto previous paragraph +### **dap_launch** +Launch debug target +### **dap_restart** +Restart debugging session +### **dap_toggle_breakpoint** +Toggle breakpoint +### **dap_continue** +Continue program execution +### **dap_pause** +Pause program execution +### **dap_step_in** +Step in +### **dap_step_out** +Step out +### **dap_next** +Step to next +### **dap_variables** +List variables +### **dap_terminate** +End debug session +### **dap_edit_condition** +Edit breakpoint condition on current line +### **dap_edit_log** +Edit breakpoint log message on current line +### **dap_switch_thread** +Switch current thread +### **dap_switch_stack_frame** +Switch stack frame +### **dap_enable_exceptions** +Enable exception breakpoints +### **dap_disable_exceptions** +Disable exception breakpoints +### **shell_pipe** +Pipe selections through shell command +### **shell_pipe_to** +Pipe selections into shell command ignoring output +### **shell_insert_output** +Insert shell command output before selections +### **shell_append_output** +Append shell command output after selections +### **shell_keep_pipe** +Filter selections with shell predicate +### **suspend** +Suspend and return to shell +### **rename_symbol** +Rename symbol +### **increment** +Increment item under cursor +### **decrement** +Decrement item under cursor +### **record_macro** +Record macro +### **replay_macro** +Replay macro +### **command_palette** +Open command palette +### **goto_word** +Jump to a two-character label +### **extend_to_word** +Extend to a two-character label +### **goto_next_tabstop** +Goto next snippet placeholder +### **goto_prev_tabstop** +Goto next snippet placeholder +### **rotate_selections_first** +Make the first selection your primary one +### **rotate_selections_last** +Make the last selection your primary one +### **insert_char** +Insert a given character at the cursor cursor position +### **insert_string** +Insert a given string at the current cursor position +### **set-current-selection-object!** +Update the selection object to the current selection within the editor +### **push-range-to-selection!** +Push a new range to a selection. The new selection will be the primary one +### **set-current-selection-primary-index!** +Set the primary index of the current selection +### **remove-current-selection-range!** +Remove a range from the current selection +### **regex-selection** +Run the given regex within the existing buffer +### **replace-selection-with** +Replace the existing selection with the given string +### **enqueue-expression-in-engine** +Enqueue an expression to run at the top level context, + after the existing function context has exited. +### **get-current-line-character** +Returns the current column number with the given position encoding +### **cx->current-file** +Get the currently focused file path +### **current_selection** +Returns the current selection as a string +### **current-selection->string** +Returns the current selection as a string +### **load-buffer!** +Evaluates the current buffer +### **current-highlighted-text!** +Returns the currently highlighted text as a string +### **get-current-line-number** +Returns the current line number +### **get-current-column-number** +Returns the visual current column number of unicode graphemes +### **current-selection-object** +Returns the current selection object +### **get-helix-cwd** +Returns the current working directly that helix is using +### **move-window-far-left** +Moves the current window to the far left +### **move-window-far-right** +Moves the current window to the far right +### **selection->primary-index** +Returns index of the primary selection +### **selection->primary-range** +Returns the range for primary selection +### **selection->ranges** +Returns all ranges of the selection +### **range-anchor** +Get the anchor of the range: the side that doesn't move when extending. +### **range->from** +Get the start of the range +### **range-head** +Get the head of the range, moved when extending. +### **range->to** +Get the end of the range +### **range->span** +Get the span of the range (from, to) +### **range** +Construct a new range object + +```scheme +(range anchor head) -> Range? +``` + +### **range->selection** +Convert a range into a selection +### **get-helix-scm-path** +Returns the path to the helix.scm file as a string +### **get-init-scm-path** +Returns the path to the init.scm file as a string +# /home/matt/.steel/cogs/helix/ext.scm +### **eval-buffer** +Eval the current buffer, morally equivalent to load-buffer! +### **evalp** +Eval prompt +### **running-on-main-thread?** +Check what the main thread id is, compare to the main thread +### **hx.with-context** +If running on the main thread already, just do nothing. +Check the ID of the engine, and if we're already on the +main thread, just continue as is - i.e. just block. This does +not block on the function if this is running on another thread. + +```scheme +(hx.with-context thunk) +``` +thunk : (-> any?) ;; Function that has no arguments + +# Examples +```scheme +(spawn-native-thread + (lambda () + (hx.with-context (lambda () (theme "nord"))))) +``` +### **hx.block-on-task** +Block on the given function. +```scheme +(hx.block-on-task thunk) +``` +thunk : (-> any?) ;; Function that has no arguments + +# Examples +```scheme +(define thread + (spawn-native-thread + (lambda () + (hx.block-on-task (lambda () (theme "nord") 10))))) + +;; Some time later, in a different context - if done at the same time, +;; this will deadline, since the join depends on the callback previously +;; executing. +(equal? (thread-join! thread) 10) ;; => #true +``` +# /home/matt/.steel/cogs/helix/components.scm +### **theme->bg** +Gets the `Style` associated with the bg for the current theme +### **theme->fg** +Gets the `style` associated with the fg for the current theme +### **theme-scope** +Get the `Style` associated with the given scope from the current theme +### **Position?** +Check if the given value is a `Position` + +```scheme +(Position? value) -> bool? +``` + +value : any? + + +### **Style?** +Check if the given valuie is `Style` + +```scheme +(Style? value) -> bool? +``` + +value : any? +### **Buffer?** + +Checks if the given value is a `Buffer` + +```scheme +(Buffer? value) -> bool? +``` + +value : any? + +### **buffer-area** + +Get the `Rect` associated with the given `Buffer` + +```scheme +(buffer-area buffer) +``` + +* buffer : Buffer? + +### **frame-set-string!** + +Set the string at the given `x` and `y` positions for the given `Buffer`, with a provided `Style`. + +```scheme +(frame-set-string! buffer x y string style) +``` + +buffer : Buffer?, +x : int?, +y : int?, +string: string?, +style: Style?, + +### **SteelEventResult?** + +Check whether the given value is a `SteelEventResult`. + +```scheme +(SteelEventResult? value) -> bool? +``` + +value : any? + + +### **new-component!** + +Construct a new dynamic component. This is used for creating widgets or floating windows +that exist outside of the buffer. This just constructs the component, it does not push the component +on to the component stack. For that, you'll use `push-component!`. + +```scheme +(new-component! name state render function-map) +``` + +name : string? - This is the name of the comoponent itself. +state : any? - Typically this is a struct that holds the state of the component. +render : (-> state? Rect? Buffer?) + This is a function that will get called with each frame. The first argument is the state object provided, + and the second is the `Rect?` to render against, ultimately against the `Buffer?`. + +function-map : (hashof string? function?) + This is a hashmap of strings -> function that contains a few important functions: + + "handle_event" : (-> state? Event?) -> SteelEventResult? + + This is called on every event with an event object. There are multiple options you can use + when returning from this function: + + * event-result/consume + * event-result/consume-without-rerender + * event-result/ignore + * event-result/close + + See the associated docs for those to understand the implications for each. + + "cursor" : (-> state? Rect?) -> Position? + + This tells helix where to put the cursor. + + "required_size": (-> state? (pair? int?)) -> (pair? int?) + + Seldom used: TODO + +### **position** + +Construct a new `Position`. + +```scheme +(position row col) -> Position? +``` + +row : int? +col : int? + +### **position-row** + +Get the row associated with the given `Position`. + +```scheme +(position-row pos) -> int? +``` + +pos : `Position?` + +### **position-col** + +Get the col associated with the given `Position`. + +```scheme +(position-col pos) -> int? +``` + +pos : `Position?` +### **set-position-row!** +Set the row for the given `Position` + +```scheme +(set-position-row! pos row) +``` + +pos : Position? +row : int? + +### **set-position-col!** +Set the col for the given `Position` + +```scheme +(set-position-col! pos col) +``` + +pos : Position? +col : int? + +### **Rect?** +Check if the given value is a `Rect` + +```scheme +(Rect? value) -> bool? +``` + +value : any? + + +### **area** + +Constructs a new `Rect`. + +(area x y width height) + +* x : int? +* y : int? +* width: int? +* height: int? + +# Examples + +```scheme +(area 0 0 100 200) +``` +### **area-x** +Get the `x` value of the given `Rect` + +```scheme +(area-x area) -> int? +``` + +area : Rect? + +### **area-y** +Get the `y` value of the given `Rect` + +```scheme +(area-y area) -> int? +``` + +area : Rect? + +### **area-width** +Get the `width` value of the given `Rect` + +```scheme +(area-width area) -> int? +``` + +area : Rect? + +### **area-height** +Get the `height` value of the given `Rect` + +```scheme +(area-height area) -> int? +``` + +area : Rect? + +### **Widget/list?** +Check whether the given value is a list widget. + +```scheme +(Widget/list? value) -> bool? +``` + +value : any? + +### **widget/list** +Creates a new `List` widget with the given items. + +```scheme +(widget/list lst) -> Widget? +``` + +* lst : (listof string?) + +### **widget/list/render** + + +Render the given `Widget/list` onto the provided `Rect` within the given `Buffer`. + +```scheme +(widget/list/render buf area lst) +``` + +* buf : `Buffer?` +* area : `Rect?` +* lst : `Widget/list?` + +### **block** +Creates a block with the following styling: + +```scheme +(block) +``` + +* borders - all +* border-style - default style + white fg +* border-type - rounded +* style - default + black bg + +### **make-block** + +Create a `Block` with the provided styling, borders, and border type. + + +```scheme +(make-block style border-style borders border_type) +``` + +* style : Style? +* border-style : Style? +* borders : string? +* border-type: String? + +Valid border-types include: +* "plain" +* "rounded" +* "double" +* "thick" + +Valid borders include: +* "top" +* "left" +* "right" +* "bottom" +* "all" + +### **block/render** + +Render the given `Block` over the given `Rect` onto the provided `Buffer`. + +```scheme +(block/render buf area block) +``` + +buf : Buffer? +area: Rect? +block: Block? + + +### **buffer/clear** +Clear a `Rect` in the `Buffer` + +```scheme +(buffer/clear area) +``` + +area : Rect? + +### **buffer/clear-with** +Clear a `Rect` in the `Buffer` with a default `Style` + +```scheme +(buffer/clear-with area style) +``` + +area : Rect? +style : Style? + +### **set-color-rgb!** + +Mutate the r/g/b of a color in place, to avoid allocation. + +```scheme +(set-color-rgb! color r g b) +``` + +color : Color? +r : int? +g : int? +b : int? +### **set-color-indexed!** + +Mutate this color to be an indexed color. + +```scheme +(set-color-indexed! color index) +``` + +color : Color? +index: int? + +### **Color?** +Check if the given value is a `Color`. + +```scheme +(Color? value) -> bool? +``` + +value : any? + + +### **Color/Reset** + +Singleton for the reset color. + +### **Color/Black** + +Singleton for the color black. + +### **Color/Red** + +Singleton for the color red. + +### **Color/White** + +Singleton for the color white. + +### **Color/Green** + +Singleton for the color green. + +### **Color/Yellow** + +Singleton for the color yellow. + +### **Color/Blue** + +Singleton for the color blue. + +### **Color/Magenta** + +Singleton for the color magenta. + +### **Color/Cyan** + +Singleton for the color cyan. + +### **Color/Gray** + +Singleton for the color gray. + +### **Color/LightRed** + +Singleton for the color light read. + +### **Color/LightGreen** + +Singleton for the color light green. + +### **Color/LightYellow** + +Singleton for the color light yellow. + +### **Color/LightBlue** + +Singleton for the color light blue. + +### **Color/LightMagenta** + +Singleton for the color light magenta. + +### **Color/LightCyan** + +Singleton for the color light cyan. + +### **Color/LightGray** + +Singleton for the color light gray. + +### **Color/rgb** + +Construct a new color via rgb. + +```scheme +(Color/rgb r g b) -> Color? +``` + +r : int? +g : int? +b : int? + +### **Color-red** + +Get the red component of the `Color?`. + +```scheme +(Color-red color) -> int? +``` + +color * Color? + +### **Color-green** + +Get the green component of the `Color?`. + +```scheme +(Color-green color) -> int? +``` + +color * Color? +### **Color-blue** + +Get the blue component of the `Color?`. + +```scheme +(Color-blue color) -> int? +``` + +color * Color? +### **Color/Indexed** + + +Construct a new indexed color. + +```scheme +(Color/Indexed index) -> Color? +``` + +* index : int? + +### **set-style-fg!** + + +Mutates the given `Style` to have the fg with the provided color. + +```scheme +(set-style-fg! style color) +``` + +style : `Style?` +color : `Color?` + +### **style-fg** + + +Constructs a new `Style` with the provided `Color` for the fg. + +```scheme +(style-fg style color) -> Style +``` + +style : Style? +color: Color? + +### **style-bg** + + +Constructs a new `Style` with the provided `Color` for the bg. + +```scheme +(style-bg style color) -> Style +``` + +style : Style? +color: Color? + +### **style-with-italics** + + +Constructs a new `Style` with italcs. + +```scheme +(style-with-italics style) -> Style +``` + +style : Style? + +### **style-with-bold** + + +Constructs a new `Style` with bold styling. + +```scheme +(style-with-bold style) -> Style +``` + +style : Style? + +### **style-with-dim** + + +Constructs a new `Style` with dim styling. + +```scheme +(style-with-dim style) -> Style +``` + +style : Style? + +### **style-with-slow-blink** + + +Constructs a new `Style` with slow blink. + +```scheme +(style-with-slow-blink style) -> Style +``` + +style : Style? + +### **style-with-rapid-blink** + + +Constructs a new `Style` with rapid blink. + +```scheme +(style-with-rapid-blink style) -> Style +``` + +style : Style? + +### **style-with-reversed** + + +Constructs a new `Style` with revered styling. + +```scheme +(style-with-reversed style) -> Style +``` + +style : Style? + +### **style-with-hidden** + +Constructs a new `Style` with hidden styling. + +```scheme +(style-with-hidden style) -> Style +``` + +style : Style? + +### **style-with-crossed-out** + + +Constructs a new `Style` with crossed out styling. + +```scheme +(style-with-crossed-out style) -> Style +``` + +style : Style? + +### **style->fg** + + +Return the color on the style, or #false if not present. + +```scheme +(style->fg style) -> (or Color? #false) +``` + +style : Style? + + +### **style->bg** + + +Return the color on the style, or #false if not present. + +```scheme +(style->bg style) -> (or Color? #false) +``` + +style : Style? + + +### **set-style-bg!** + + +Mutate the background style on the given style to a given color. + +```scheme +(set-style-bg! style color) +``` + +style : Style? +color : Color? + + +### **style-underline-color** + + +Return a new style with the provided underline color. + +```scheme +(style-underline-color style color) -> Style? + +``` +style : Style? +color : Color? + + +### **style-underline-style** + +Return a new style with the provided underline style. + +```scheme +(style-underline-style style underline-style) -> Style? + +``` + +style : Style? +underline-style : UnderlineStyle? + +### **UnderlineStyle?** + +Check if the provided value is an `UnderlineStyle`. + +```scheme +(UnderlineStyle? value) -> bool? + +``` +value : any? +### **Underline/Reset** + +Singleton for resetting the underling style. + +### **Underline/Line** + +Singleton for the line underline style. + +### **Underline/Curl** + +Singleton for the curl underline style. + +### **Underline/Dotted** + +Singleton for the dotted underline style. + +### **Underline/Dashed** + +Singleton for the dashed underline style. + +### **Underline/DoubleLine** + +Singleton for the double line underline style. + +### **event-result/consume** + +Singleton for consuming an event. If this is returned from an event handler, the event +will not continue to be propagated down the component stack. This also will trigger a +re-render. + +### **event-result/consume-without-rerender** + +Singleton for consuming an event. If this is returned from an event handler, the event +will not continue to be propagated down the component stack. This will _not_ trigger +a re-render. + +### **event-result/ignore** + +Singleton for ignoring an event. If this is returned from an event handler, the event +will not continue to be propagated down the component stack. This will _not_ trigger +a re-render. + +### **event-result/ignore-and-close** + +Singleton for ignoring an event. If this is returned from an event handler, the event +will continue to be propagated down the component stack, and the component will be +popped off of the stack and removed. + +### **event-result/close** + +Singleton for consuming an event. If this is returned from an event handler, the event +will not continue to be propagated down the component stack, and the component will +be popped off of the stack and removed. + +### **style** + +Constructs a new default style. + +```scheme +(style) -> Style? +``` + +### **Event?** +Check if this value is an `Event` + +```scheme +(Event? value) -> bool? +``` +value : any? + +### **paste-event?** +Checks if the given event is a paste event. + +```scheme +(paste-event? event) -> bool? +``` + +* event : Event? + + +### **paste-event-string** +Get the string from the paste event, if it is a paste event. + +```scheme +(paste-event-string event) -> (or string? #false) +``` + +* event : Event? + + +### **key-event?** +Checks if the given event is a key event. + +```scheme +(key-event? event) -> bool? +``` + +* event : Event? + +### **string->key-event** +Get a key event from a string +### **event->key-event** +Return the key event from an event, if it is one +### **key-event-char** +Get the character off of the event, if there is one. + +```scheme +(key-event-char event) -> (or char? #false) +``` +event : Event? + +### **key-event-modifier** + +Get the key event modifier off of the event, if there is one. + +```scheme +(key-event-modifier event) -> (or int? #false) +``` +event : Event? + +### **key-modifier-ctrl** + +The key modifier bits associated with the ctrl key modifier. + +### **key-modifier-shift** + +The key modifier bits associated with the shift key modifier. + +### **key-modifier-alt** + +The key modifier bits associated with the alt key modifier. + +### **key-modifier-super** + +The key modifier bits associated with the super key modifier. + +### **key-event-F?** +Check if this key event is associated with an `F` key, e.g. F1, F2, etc. + +```scheme +(key-event-F? event number) -> bool? +``` +event : Event? +number : int? + +### **mouse-event?** + +Check if this event is a mouse event. + +```scheme +(mouse-event event) -> bool? +``` +event : Event? +### **event-mouse-kind** +Convert the mouse event kind into an integer representing the state. + +```scheme +(event-mouse-kind event) -> (or int? #false) +``` + +event : Event? + +This is the current mapping today: + +```rust +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, +} +``` + +Any unhandled event that does not match this will return `#false`. +### **event-mouse-row** + + +Get the row from the mouse event, of #false if it isn't a mouse event. + +```scheme +(event-mouse-row event) -> (or int? #false) +``` + +event : Event? + + +### **event-mouse-col** + + +Get the col from the mouse event, of #false if it isn't a mouse event. + +```scheme +(event-mouse-row event) -> (or int? #false) +``` + +event : Event? + +### **mouse-event-within-area?** +Check whether the given mouse event occurred within a given `Rect`. + +```scheme +(mouse-event-within-area? event area) -> bool? +``` + +event : Event? +area : Rect? + +### **key-event-escape?** + +Check whether the given event is the key: escape + +```scheme +(key-event-escape? event) +``` +event: Event? +### **key-event-backspace?** + +Check whether the given event is the key: backspace + +```scheme +(key-event-backspace? event) +``` +event: Event? +### **key-event-enter?** + +Check whether the given event is the key: enter + +```scheme +(key-event-enter? event) +``` +event: Event? +### **key-event-left?** + +Check whether the given event is the key: left + +```scheme +(key-event-left? event) +``` +event: Event? +### **key-event-right?** + +Check whether the given event is the key: right + +```scheme +(key-event-right? event) +``` +event: Event? +### **key-event-up?** + +Check whether the given event is the key: up + +```scheme +(key-event-up? event) +``` +event: Event? +### **key-event-down?** + +Check whether the given event is the key: down + +```scheme +(key-event-down? event) +``` +event: Event? +### **key-event-home?** + +Check whether the given event is the key: home + +```scheme +(key-event-home? event) +``` +event: Event? +### **key-event-end?** + +Check whether the given event is the key: end + +```scheme +(key-event-end? event) +``` +event: Event? +### **key-event-page-up?** + +Check whether the given event is the key: page-up + +```scheme +(key-event-page-up? event) +``` +event: Event? +### **key-event-page-down?** + +Check whether the given event is the key: page-down + +```scheme +(key-event-page-down? event) +``` +event: Event? +### **key-event-tab?** + +Check whether the given event is the key: tab + +```scheme +(key-event-tab? event) +``` +event: Event? +### **key-event-delete?** + +Check whether the given event is the key: delete + +```scheme +(key-event-delete? event) +``` +event: Event? +### **key-event-insert?** + +Check whether the given event is the key: insert + +```scheme +(key-event-insert? event) +``` +event: Event? +### **key-event-null?** + +Check whether the given event is the key: null + +```scheme +(key-event-null? event) +``` +event: Event? +### **key-event-caps-lock?** + +Check whether the given event is the key: caps-lock + +```scheme +(key-event-caps-lock? event) +``` +event: Event? +### **key-event-scroll-lock?** + +Check whether the given event is the key: scroll-lock + +```scheme +(key-event-scroll-lock? event) +``` +event: Event? +### **key-event-num-lock?** + +Check whether the given event is the key: num-lock + +```scheme +(key-event-num-lock? event) +``` +event: Event? +### **key-event-print-screen?** + +Check whether the given event is the key: print-screen + +```scheme +(key-event-print-screen? event) +``` +event: Event? +### **key-event-pause?** + +Check whether the given event is the key: pause + +```scheme +(key-event-pause? event) +``` +event: Event? +### **key-event-menu?** + +Check whether the given event is the key: menu + +```scheme +(key-event-menu? event) +``` +event: Event? +### **key-event-keypad-begin?** + +Check whether the given event is the key: keypad-begin + +```scheme +(key-event-keypad-begin? event) +``` +event: Event? +# helix/core/text +To use, you can include with `(require-builtin helix/core/text)` +### **Rope?** +Check if the given value is a rope +### **RopeRegex?** +Check if the given value is a rope regex +### **rope->byte-slice** +Take a slice of this rope using byte offsets + +```scheme +(rope->byte-slice rope start end) -> Rope? +``` + +* rope: Rope? +* start: (and positive? int?) +* end: (and positive? int?) +### **rope->line** +Get the line at the given line index. Returns a rope. + +```scheme +(rope->line rope index) -> Rope? + +``` + +* rope : Rope? +* index : (and positive? int?) +### **rope->slice** +Take a slice from using character indices from the rope. +Returns a new rope value. + +```scheme +(rope->slice rope start end) -> Rope? +``` + +* rope : Rope? +* start: (and positive? int?) +* end: (and positive? int?) +### **rope->string** +Convert the given rope to a string +### **rope-byte->line** +Convert the given byte offset to a line offset for a given rope + +```scheme +(rope-byte->line rope byte-index) -> int? +``` + +* rope : Rope? +* byte-index : int? + + +### **rope-char->byte** +Convert the byte offset into a character offset for a given rope +### **rope-char->line** +Convert the given character offset to a line offset for a given rope + +```scheme +(rope-char->line rope char-index) -> int? +``` + +* rope : Rope? +* char-index : int? + + +### **rope-char-ref** +Get the character at the given index +### **rope-ends-with?** +Check if the rope ends with a given pattern +### **rope-insert-char** +Insert a character at the given index +### **rope-insert-string** +Insert a string at the given index into the rope +### **rope-len-bytes** +Get the length of the rope in bytes +### **rope-len-chars** +Get the length of the rope in characters +### **rope-len-lines** +Get the number of lines in the rope +### **rope-line->byte** +Convert the given line index to a byte offset for a given rope + +```scheme +(rope-line->byte rope line-offset) -> int? +``` + +* rope : Rope? +* line-offset: int? + +### **rope-line->char** +Convert the given line index to a character offset for a given rope + +```scheme +(rope-line->char rope line-offset) -> int? +``` + +* rope : Rope? +* line-offset: int? + +### **rope-regex** +Build a new RopeRegex? with a string + +```scheme +(rope-regex string) -> RopeRegex? +``` + +* string: string? + +### **rope-regex-find** +Find the first match in a given rope + +```scheme +(rope-regex-find regex rope) -> Rope? +``` + +* regex: RopeRegex? +* rope: Rope? + +### **rope-regex-find\*** +Find and return all matches in a given rope + +```scheme +(rope-regex-find* regex rope) -> '(Rope?) +``` +* regex: RopeRegex? +* rope: Rope? + +### **rope-regex-match?** +Returns if a regex is matching on a given rope + +```scheme +(rope-regex->match? regex rope) -> bool? +``` + +* regex: RopeRegex? +* rope: Rope? + +### **rope-regex-split** +Split on the match in a given rope + +```scheme +(rope-regex-split regex rope) -> '(Rope?) +``` + +* regex: RopeRegex? +* rope: Rope? +### **rope-regex-splitn** +Split n times on the match in a given rope, return the rest + +```scheme +(rope-regex-splitn regex rope n) -> '(Rope?) +``` + +* regex: RopeRegex? +* rope: Rope? +* n: (and positive? int?) +### **rope-starts-with?** +Check if the rope starts with a given pattern +### **rope-trim-start** +Remove the leading whitespace from the given rope +### **string->rope** +Converts a string into a rope. + +```scheme +(string->rope value) -> Rope? +``` + +* value : string? + 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 7bb7e8c90c42..ca52135ad087 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,3 +1,4 @@ +mod codegen; mod docgen; mod helpers; mod path; @@ -7,6 +8,7 @@ use std::{env, error::Error}; type DynError = Box; pub mod tasks { + use crate::codegen::code_gen; use crate::DynError; use std::collections::HashSet; @@ -45,6 +47,74 @@ pub mod tasks { Ok(()) } + pub fn codegen() { + code_gen() + } + + pub fn install_steel() { + std::process::Command::new("cargo") + .args([ + "install", + "--git", + "https://github.com/mattwparas/steel.git", + "steel-interpreter", + "steel-language-server", + "cargo-steel-lib", + "--locked", + "--force", + ]) + .spawn() + .unwrap() + .wait() + .unwrap(); + + std::process::Command::new("cargo") + .args([ + "install", + "--git", + "https://github.com/mattwparas/steel.git", + "steel-forge", + "--locked", + "--force", + ]) + .spawn() + .unwrap() + .wait() + .unwrap(); + + println!("----------------------------"); + println!("=> Finished installing steel"); + println!("----------------------------"); + println!("Warming up `forge`..."); + + std::process::Command::new("forge") + .args(["pkg", "refresh"]) + .spawn() + .unwrap() + .wait() + .unwrap(); + + println!("Done."); + println!("----------------------------"); + + code_gen(); + + std::process::Command::new("cargo") + .args([ + "install", + "--path", + "helix-term", + "--features", + "steel,git", + "--locked", + "--force", + ]) + .spawn() + .unwrap() + .wait() + .unwrap(); + } + pub fn themecheck(themes: impl Iterator) -> Result<(), DynError> { use helix_view::theme::Loader; @@ -89,6 +159,8 @@ pub mod tasks { Usage: Run with `cargo xtask `, eg. `cargo xtask docgen`. Tasks: + steel Install steel and helix with steel enabled + code-gen Generate any code used for steel docgen Generate files to be included in the mdbook output. query-check [languages] Check that tree-sitter queries are valid for the given languages, or all languages if none are specified. @@ -106,6 +178,8 @@ fn main() -> Result<(), DynError> { None => tasks::print_help(), Some(t) => match t.as_str() { "docgen" => tasks::docgen()?, + "code-gen" => tasks::codegen(), + "steel" => tasks::install_steel(), "query-check" => tasks::querycheck(args)?, "theme-check" => tasks::themecheck(args)?, invalid => return Err(format!("Invalid task name: {}", invalid).into()),