diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000000..a301bb9070b8 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "steel"] + path = steel + url = https://github.com/mattwparas/steel.git diff --git a/Cargo.lock b/Cargo.lock index 49a417890bc3..1b2406830a42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,19 +3,61 @@ version = 3 [[package]] -name = "addr2line" -version = "0.22.0" +name = "abi_stable" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "69d6512d3eb05ffe5004c59c206de7f99c34951504056ce23fc953842f12c445" dependencies = [ - "gimli", + "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 = "adler" -version = "1.0.2" +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 = "addr2line" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] [[package]] name = "adler2" @@ -68,9 +110,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.89" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" [[package]] name = "arc-swap" @@ -78,33 +120,97 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "as_derive_utils" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff3c96645900a44cf11941c111bd08a6573b0e2f9f69bc9264b179d8fae753c4" +dependencies = [ + "core_extensions", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "async-ffi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4de21c0feef7e5a556e51af767c953f0501f7f300ba785cc99c47bdc8081a50" +dependencies = [ + "abi_stable", +] + [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", - "miniz_oxide 0.7.4", + "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "bigdecimal" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d712318a27c7150326677b321a5fa91b55f6d9034ffd67f20319e147d40cee" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + [[package]] name = "bstr" version = "1.10.0" @@ -122,11 +228,17 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "cassowary" @@ -134,11 +246,20 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" -version = "1.1.23" +version = "1.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bbb537bb4a30b90362caddba8f360c0a56bc13d3a5570028e7197204cb54a17" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" dependencies = [ "shlex", ] @@ -187,6 +308,55 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59" +[[package]] +name = "codegen" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff61280aed771c3070e7dcc9e050c66f1eb1e3b96431ba66f9f74641d02fc41d" +dependencies = [ + "indexmap 1.9.3", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "compact_str" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" +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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "013b6c2c3a14d678f38cd23994b02da3a1a1b6a5d1eedddfe63a5a5f11b13a81" + [[package]] name = "content_inspector" version = "0.2.4" @@ -196,12 +366,36 @@ dependencies = [ "memchr", ] +[[package]] +name = "coolor" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4d7a805ca0d92f8c61a31c809d4323fdaa939b0b440e544d21db7797c5aaad" +dependencies = [ + "crossterm 0.23.2", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core_extensions" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92c71dc07c9721607e7a16108336048ee978c3a8b129294534272e8bac96c0ee" +dependencies = [ + "core_extensions_proc_macros", +] + +[[package]] +name = "core_extensions_proc_macros" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f3b219d28b6e3b4ac87bc1fc522e0803ab22e055da177bff0068c4150c61a6" + [[package]] name = "crc32fast" version = "1.4.2" @@ -211,6 +405,28 @@ dependencies = [ "cfg-if", ] +[[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.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -230,24 +446,49 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crossterm" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2102ea4f781910f8a5b98dd061f4c2023f479ce7bb1236330099ceb5a93cf17" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags", + "bitflags 2.6.0", "crossterm_winapi", "filedescriptor", "futures-core", "libc", - "mio", + "mio 1.0.2", "parking_lot", "rustix", "signal-hook", @@ -272,12 +513,33 @@ checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" dependencies = [ "cfg-if", "crossbeam-utils", - "hashbrown", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "dunce" version = "1.0.5" @@ -326,9 +588,9 @@ dependencies = [ [[package]] name = "error-code" -version = "3.2.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" +checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" [[package]] name = "etcetera" @@ -387,12 +649,12 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.33" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", - "miniz_oxide 0.8.0", + "miniz_oxide", ] [[package]] @@ -412,40 +674,70 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", "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.85", +] + [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", + "futures-macro", "futures-task", "pin-project-lite", "pin-utils", "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generational-arena" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877e94aff08e743b651baaea359664321055749b398adff8740a7399af7796e7" +dependencies = [ + "cfg-if", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -453,15 +745,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "gix" @@ -544,27 +838,27 @@ dependencies = [ [[package]] name = "gix-bitmap" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a371db66cbd4e13f0ed9dc4c0fea712d7276805fccc877f77e96374d317e87ae" +checksum = "10f78312288bd02052be5dbc2ecbc342c9f4eb791986d86c0a5c06b92dc72efa" dependencies = [ "thiserror", ] [[package]] name = "gix-chunk" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c8751169961ba7640b513c3b24af61aa962c967aaf04116734975cd5af0c52" +checksum = "6c28b58ba04f0c004722344390af9dbc85888fbb84be1981afb934da4114d4cf" dependencies = [ "thiserror", ] [[package]] name = "gix-command" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff2e692b36bbcf09286c70803006ca3fd56551a311de450be317a0ab8ea92e7" +checksum = "c201d2b9e9cce2365a6638fd0a966f751ed92d74be5c0727ac331e6a29ef5846" dependencies = [ "bstr", "gix-path", @@ -609,11 +903,11 @@ dependencies = [ [[package]] name = "gix-config-value" -version = "0.14.8" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03f76169faa0dec598eac60f83d7fcdd739ec16596eca8fb144c88973dbe6f8c" +checksum = "f3de3fdca9c75fa4b83a76583d265fa49b1de6b088ebcd210749c24ceeb74660" dependencies = [ - "bitflags", + "bitflags 2.6.0", "bstr", "gix-path", "libc", @@ -622,9 +916,9 @@ dependencies = [ [[package]] name = "gix-date" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c84b7af01e68daf7a6bb8bb909c1ff5edb3ce4326f1f43063a5a96d3c3c8a5" +checksum = "d10d543ac13c97292a15e8e8b7889cd006faf739777437ed95362504b8fe81a0" dependencies = [ "bstr", "itoa", @@ -745,7 +1039,7 @@ version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74908b4bbc0a0a40852737e5d7889f676f081e340d5451a16e5b4c50d592f111" dependencies = [ - "bitflags", + "bitflags 2.6.0", "bstr", "gix-features", "gix-path", @@ -768,7 +1062,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ddf80e16f3c19ac06ce415a38b8591993d3f73aede049cb561becb5b3a8e242" dependencies = [ "gix-hash", - "hashbrown", + "hashbrown 0.14.5", "parking_lot", ] @@ -791,7 +1085,7 @@ version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cd4203244444017682176e65fd0180be9298e58ed90bd4a8489a357795ed22d" dependencies = [ - "bitflags", + "bitflags 2.6.0", "bstr", "filetime", "fnv", @@ -804,7 +1098,7 @@ dependencies = [ "gix-traverse", "gix-utils", "gix-validate", - "hashbrown", + "hashbrown 0.14.5", "itoa", "libc", "memmap2", @@ -895,9 +1189,9 @@ dependencies = [ [[package]] name = "gix-path" -version = "0.10.11" +version = "0.10.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebfc4febd088abdcbc9f1246896e57e37b7a34f6909840045a1767c6dafac7af" +checksum = "c04e5a94fdb56b1e91eb7df2658ad16832428b8eeda24ff1a0f0288de2bce554" dependencies = [ "bstr", "gix-trace", @@ -912,7 +1206,7 @@ version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d23bf239532b4414d0e63b8ab3a65481881f7237ed9647bb10c1e3cc54c5ceb" dependencies = [ - "bitflags", + "bitflags 2.6.0", "bstr", "gix-attributes", "gix-config-value", @@ -923,9 +1217,9 @@ dependencies = [ [[package]] name = "gix-quote" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbff4f9b9ea3fa7a25a70ee62f545143abef624ac6aa5884344e70c8b0a1d9ff" +checksum = "f89f9a1525dcfd9639e282ea939f5ab0d09d93cf2b90c1fc6104f1b9582a8e49" dependencies = [ "bstr", "gix-utils", @@ -998,11 +1292,11 @@ dependencies = [ [[package]] name = "gix-sec" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe4d52f30a737bbece5276fab5d3a8b276dc2650df963e293d0673be34e7a5f" +checksum = "a2007538eda296445c07949cf04f4a767307d887184d6b3e83e2d636533ddc6e" dependencies = [ - "bitflags", + "bitflags 2.6.0", "gix-path", "libc", "windows-sys 0.52.0", @@ -1062,9 +1356,9 @@ dependencies = [ [[package]] name = "gix-trace" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cae0e8661c3ff92688ce1c8b8058b3efb312aba9492bbe93661a21705ab431b" +checksum = "04bdde120c29f1fc23a24d3e115aeeea3d60d8e65bab92cc5f9d90d9302eb952" [[package]] name = "gix-traverse" @@ -1072,7 +1366,7 @@ version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "030da39af94e4df35472e9318228f36530989327906f38e27807df305fccb780" dependencies = [ - "bitflags", + "bitflags 2.6.0", "gix-commitgraph", "gix-date", "gix-hash", @@ -1099,9 +1393,9 @@ dependencies = [ [[package]] name = "gix-utils" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35192df7fd0fa112263bad8021e2df7167df4cc2a6e6d15892e1e55621d3d4dc" +checksum = "ba427e3e9599508ed98a6ddf8ed05493db114564e338e41f6a996d2e4790335f" dependencies = [ "bstr", "fastrand", @@ -1110,9 +1404,9 @@ dependencies = [ [[package]] name = "gix-validate" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f2badbb64e57b404593ee26b752c26991910fd0d81fe6f9a71c1a8309b6c86" +checksum = "e187b263461bc36cea17650141567753bc6207d036cedd1de6e81a52f277ff68" dependencies = [ "bstr", "thiserror", @@ -1187,6 +1481,12 @@ dependencies = [ "memmap2", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -1195,21 +1495,28 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", + "serde", ] +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + [[package]] name = "helix-core" version = "24.7.0" dependencies = [ "ahash", "arc-swap", - "bitflags", + "bitflags 2.6.0", "chrono", "dunce", "encoding_rs", "etcetera", "globset", - "hashbrown", + "hashbrown 0.14.5", "helix-loader", "helix-stdx", "imara-diff", @@ -1226,6 +1533,7 @@ dependencies = [ "slotmap", "smallvec", "smartstring", + "steel-core", "textwrap", "toml", "tree-sitter", @@ -1257,7 +1565,7 @@ dependencies = [ "ahash", "anyhow", "futures-executor", - "hashbrown", + "hashbrown 0.14.5", "log", "once_cell", "parking_lot", @@ -1273,7 +1581,7 @@ dependencies = [ "dunce", "etcetera", "helix-stdx", - "libloading", + "libloading 0.8.5", "log", "once_cell", "serde", @@ -1311,7 +1619,7 @@ dependencies = [ name = "helix-lsp-types" version = "0.95.1" dependencies = [ - "bitflags", + "bitflags 2.6.0", "serde", "serde_json", "serde_repr", @@ -1326,14 +1634,14 @@ version = "24.7.0" name = "helix-stdx" version = "24.7.0" dependencies = [ - "bitflags", + "bitflags 2.6.0", "dunce", "etcetera", "regex-cursor", "ropey", "rustix", "tempfile", - "which", + "which 6.0.3", "windows-sys 0.59.0", ] @@ -1345,7 +1653,7 @@ dependencies = [ "arc-swap", "chrono", "content_inspector", - "crossterm", + "crossterm 0.28.1", "fern", "futures-util", "grep-regex", @@ -1373,6 +1681,8 @@ dependencies = [ "signal-hook", "signal-hook-tokio", "smallvec", + "steel-core", + "steel-doc", "tempfile", "termini", "thiserror", @@ -1386,14 +1696,15 @@ dependencies = [ name = "helix-tui" version = "24.7.0" dependencies = [ - "bitflags", + "bitflags 2.6.0", "cassowary", - "crossterm", + "crossterm 0.28.1", "helix-core", "helix-view", "log", "once_cell", "serde", + "steel-core", "termini", "unicode-segmentation", ] @@ -1420,10 +1731,10 @@ version = "24.7.0" dependencies = [ "anyhow", "arc-swap", - "bitflags", + "bitflags 2.6.0", "chardetng", "clipboard-win", - "crossterm", + "crossterm 0.28.1", "futures-util", "helix-core", "helix-dap", @@ -1441,6 +1752,7 @@ dependencies = [ "serde", "serde_json", "slotmap", + "steel-core", "tempfile", "thiserror", "tokio", @@ -1455,6 +1767,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "home" version = "0.5.9" @@ -1464,11 +1782,28 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1514,42 +1849,89 @@ dependencies = [ ] [[package]] -name = "imara-diff" -version = "0.1.7" +name = "im" +version = "15.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc9da1a252bd44cd341657203722352efc9bc0c847d06ea6d2dc1cd1135e0a01" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" dependencies = [ - "ahash", - "hashbrown", + "bitmaps", + "rand_core", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", ] [[package]] -name = "indexmap" -version = "2.5.0" +name = "im-lists" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "c38da2a11f128e1af1585abdd7041a96fc1adcd6ca2cb33e7fc5728326edbe8a" dependencies = [ - "equivalent", - "hashbrown", + "smallvec", ] [[package]] -name = "indoc" -version = "2.0.5" +name = "im-rc" +version = "15.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" +checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe" +dependencies = [ + "bitmaps", + "rand_core", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] [[package]] -name = "is-docker" -version = "0.2.0" +name = "imara-diff" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +checksum = "fc9da1a252bd44cd341657203722352efc9bc0c847d06ea6d2dc1cd1135e0a01" dependencies = [ - "once_cell", + "ahash", + "hashbrown 0.14.5", ] [[package]] -name = "is-wsl" +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.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.0", +] + +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" @@ -1591,9 +1973,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] @@ -1607,11 +1989,33 @@ 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 = "libc" -version = "0.2.159" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] [[package]] name = "libloading" @@ -1623,13 +2027,19 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "libredox" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags", + "bitflags 2.6.0", "libc", "redox_syscall", ] @@ -1664,20 +2074,20 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memmap2" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" dependencies = [ "libc", ] [[package]] -name = "miniz_oxide" -version = "0.7.4" +name = "minimad" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +checksum = "fed1b13e2000bd8e238d97a97de6fc30224f89a08b0aa5aaa09ed1bd68ba2fa1" dependencies = [ - "adler", + "once_cell", ] [[package]] @@ -1689,13 +2099,25 @@ dependencies = [ "adler2", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "mio" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", "log", "wasi", @@ -1723,6 +2145,73 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1738,27 +2227,24 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", ] [[package]] name = "object" -version = "0.36.4" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.20.1" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" -dependencies = [ - "portable-atomic", -] +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "open" @@ -1771,6 +2257,12 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "parking_lot" version = "0.12.3" @@ -1794,11 +2286,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathdiff" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361" [[package]] name = "percent-encoding" @@ -1818,17 +2316,52 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "polling" +version = "3.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "portable-atomic" -version = "1.7.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55c4d17d994b637e2f4daf6e5dc5d660d209d5642377d675d7a1c3ab69fa579" +dependencies = [ + "arrayvec", + "typed-arena", + "unicode-width", +] [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] @@ -1839,13 +2372,22 @@ version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79" +[[package]] +name = "psm" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa37f80ca58604976033fae9515a8a2989fc13797d953f7c04fb8fa36a11f205" +dependencies = [ + "cc", +] + [[package]] name = "pulldown-cmark" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666f0f59e259aea2d72e6012290c09877a780935cc3c18b1ceded41f3890d59c" +checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" dependencies = [ - "bitflags", + "bitflags 2.6.0", "memchr", "unicase", ] @@ -1859,6 +2401,16 @@ dependencies = [ "rand", ] +[[package]] +name = "quickscope" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d47bcfc3e13850589cf9338a02b6dfb5aebb3748a0f93a392e8df91d6193b6b" +dependencies = [ + "indexmap 1.9.3", + "smallvec", +] + [[package]] name = "quote" version = "1.0.37" @@ -1868,12 +2420,30 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radix_fmt" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce082a9940a7ace2ad4a8b7d0b1eac6aa378895f18be598230c5f2284ac05426" + [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", "rand_core", ] @@ -1886,6 +2456,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core", +] + [[package]] name = "rayon" version = "1.10.0" @@ -1908,11 +2487,22 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.3" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "bitflags", + "bitflags 2.6.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", ] [[package]] @@ -1957,6 +2547,15 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "repr_offset" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1070755bd29dffc19d0971cab794e607839ba2ef4b69a9e6fbc8733c1b72ea" +dependencies = [ + "tstr", +] + [[package]] name = "ropey" version = "1.6.1" @@ -1973,19 +2572,34 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ - "bitflags", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + [[package]] name = "ryu" version = "1.0.18" @@ -2007,31 +2621,37 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "serde" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.85", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", "memchr", @@ -2047,14 +2667,14 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.85", ] [[package]] name = "serde_spanned" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -2094,7 +2714,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio", + "mio 0.8.11", + "mio 1.0.2", "signal-hook", ] @@ -2119,6 +2740,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + [[package]] name = "slab" version = "0.4.9" @@ -2170,23 +2801,147 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "stacker" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] + [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "steel-core" +version = "0.6.0" +source = "git+https://github.com/mattwparas/steel.git#c89f53b6336c8eb3592328946149b8ebbd43b05a" +dependencies = [ + "abi_stable", + "anyhow", + "async-ffi", + "bigdecimal", + "bincode", + "chrono", + "codespan-reporting", + "compact_str", + "crossbeam", + "dirs", + "futures-executor", + "futures-task", + "futures-util", + "fxhash", + "getrandom", + "home", + "http", + "httparse", + "im", + "im-lists", + "im-rc", + "lasso", + "log", + "num", + "once_cell", + "parking_lot", + "polling", + "pretty", + "quickscope", + "radix_fmt", + "rand", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "stacker", + "steel-derive", + "steel-gen", + "steel-parser", + "strsim", + "termimad", + "weak-table", + "which 4.4.2", +] + +[[package]] +name = "steel-derive" +version = "0.5.0" +source = "git+https://github.com/mattwparas/steel.git#c89f53b6336c8eb3592328946149b8ebbd43b05a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "steel-doc" +version = "0.6.0" +source = "git+https://github.com/mattwparas/steel.git#c89f53b6336c8eb3592328946149b8ebbd43b05a" +dependencies = [ + "steel-core", +] + +[[package]] +name = "steel-gen" +version = "0.2.0" +source = "git+https://github.com/mattwparas/steel.git#c89f53b6336c8eb3592328946149b8ebbd43b05a" +dependencies = [ + "codegen", + "serde", + "serde_derive", +] + +[[package]] +name = "steel-parser" +version = "0.6.0" +source = "git+https://github.com/mattwparas/steel.git#c89f53b6336c8eb3592328946149b8ebbd43b05a" +dependencies = [ + "compact_str", + "fxhash", + "lasso", + "num", + "once_cell", + "pretty", + "serde", + "serde_derive", + "smallvec", +] + [[package]] name = "str_indices" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9557cb6521e8d009c51a8666f09356f4b817ba9ba0981a305bd86aee47bd35c" +[[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 = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" -version = "2.0.77" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -2206,6 +2961,29 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "termimad" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8743d628f9b0eb33087c1e1c4915d91efca23ae69f7c81981489128a0e17d300" +dependencies = [ + "coolor", + "crossbeam", + "crossterm 0.23.2", + "minimad", + "thiserror", + "unicode-width", +] + [[package]] name = "termini" version = "1.0.0" @@ -2228,22 +3006,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.85", ] [[package]] @@ -2272,14 +3050,14 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.40.0" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" dependencies = [ "backtrace", "bytes", "libc", - "mio", + "mio 1.0.2", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -2296,7 +3074,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.85", ] [[package]] @@ -2333,17 +3111,33 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.20" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap", + "indexmap 2.6.0", "serde", "serde_spanned", "toml_datetime", "winnow", ] +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" + [[package]] name = "tree-sitter" version = "0.22.6" @@ -2355,19 +3149,43 @@ dependencies = [ ] [[package]] -name = "unicase" -version = "2.7.0" +name = "tstr" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +checksum = "7f8e0294f14baae476d0dd0a2d780b2e24d66e349a9de876f5126777a37bdba7" dependencies = [ - "version_check", + "tstr_proc_macros", ] +[[package]] +name = "tstr_proc_macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78122066b0cb818b8afd08f7ed22f7fdbc3e90815035726f0840d0d26c0747a" + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicase" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" + [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-bom" @@ -2383,9 +3201,9 @@ checksum = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-linebreak" @@ -2395,9 +3213,9 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] @@ -2450,9 +3268,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", "once_cell", @@ -2461,24 +3279,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.85", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2486,22 +3304,40 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.85", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "weak-table" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "323f4da9523e9a669e1eaf9c6e763892769b1d38c623913647bfdc1532fe4549" + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] [[package]] name = "which" @@ -2705,9 +3541,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] @@ -2735,6 +3571,7 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] @@ -2746,5 +3583,5 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.85", ] diff --git a/Cargo.toml b/Cargo.toml index 763992480176..b07a84d1daeb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ default-members = [ [profile.release] lto = "thin" -# debug = true +debug = true [profile.opt] inherits = "release" @@ -42,6 +42,9 @@ tree-sitter = { version = "0.22" } nucleo = "0.5.0" slotmap = "1.0.7" thiserror = "1.0" +# If working locally, use the local path dependency +# steel-core = { path = "/Users/matt/code/steel/crates/steel-core", version = "0.6.0", features = ["anyhow", "dylibs", "sync"] } +steel-core = { git = "https://github.com/mattwparas/steel.git", version = "0.6.0", features = ["anyhow", "dylibs", "sync"] } [workspace.package] version = "24.7.0" diff --git a/STEEL.md b/STEEL.md new file mode 100644 index 000000000000..1120f3a0eeba --- /dev/null +++ b/STEEL.md @@ -0,0 +1,166 @@ +# Building + +You will need: + +* A clone of this fork, on the branch `steel-event-system` + +`steel` is included as a git submodule for ease of building. + +## Installing helix + +Just run + +`cargo xtask steel` + +To install the `hx` executable, with steel as a plugin language. This also includes: + +The `steel` executable, the steel language server, the steel dylib installer, and the steel standard library. + + +## Setting up configurations for helix + +Note, this API is entirely subjet to change, and I promise absolutely 0 backwards compatibility while this is in development. + +There are 2 important files you'll want, which should be auto generated during the installation process: + +* `~/.config/helix/helix.scm` +* `~/.config/helix/init.scm` + +Note - these both live inside the same directory that helix sets up for runtime configurations. + + +### `helix.scm` + +The `helix.scm` module will be loaded first before anything else, the runtime will `require` this module, and any functions exported will now be available +to be used as typed commands. For example: + + +```scheme +# helix.scm +(require "helix/editor.scm") +(require (prefix-in helix. "helix/commands.scm")) +(require (prefix-in helix.static. "helix/static.scm")) + +(provide shell git-add open-helix-scm open-init-scm) + +;;@doc +;; Specialized shell implementation, where % is a wildcard for the current file +(define (shell cx . args) + ;; Replace the % with the current file + (define expanded (map (lambda (x) (if (equal? x "%") (current-path cx) x)) args)) + (apply helix.run-shell-command expanded)) + +;;@doc +;; Adds the current file to git +(define (git-add cx) + (shell cx "git" "add" "%")) + +(define (current-path) + (let* ([focus (editor-focus)] + [focus-doc-id (editor->doc-id focus)]) + (editor-document->path focus-doc-id))) + +;;@doc +;; Open the helix.scm file +(define (open-helix-scm) + (helix.open (helix.static.get-helix-scm-path))) + +;;@doc +;; Opens the init.scm file +(define (open-init-scm) + (helix.open (helix.static.get-init-scm-path))) + + +``` + +Now, if you'd like to add the current file you're editing to git, simply type `:git-add` - you'll see the doc pop up with it since we've annotated the function +with the `@doc` symbol. Hitting enter will execute the command. + +You can also conveniently open the `helix.scm` file by using the typed command `:open-helix-scm`. + + +### `init.scm` + +The `init.scm` file is run at the top level, immediately after the `helix.scm` module is `require`d. The helix context is available here, so you can interact with the editor. + +The helix context is bound to the top level variable `*helix.cx*`. + +For example, if we wanted to select a random theme at startup: + +```scheme +# init.scm + +(require-builtin steel/random as rand::) +(require (prefix-in helix. "helix/commands.scm")) +(require (prefix-in helix.static. "helix/static.scm")) + +(define rng (rand::thread-rng!)) + +;; Picking one from the possible themes +(define possible-themes '("ayu_mirage" "tokyonight_storm" "catppuccin_macchiato")) + +(define (select-random lst) + (let ([index (rand::rng->gen-range rng 0 (length lst))]) (list-ref lst index))) + +(define (randomly-pick-theme options) + ;; Randomly select the theme from the possible themes list + (helix.theme (select-random options))) + +(randomly-pick-theme possible-themes) + +``` + +### Libraries for helix + +There are a handful of extra libraries in development for extending helix, and can be found here https://github.com/mattwparas/helix-config. + +If you'd like to use them, create a directory called `cogs` in your `.config/helix` directory, and copy the files in there. + +### options.scm + +If you'd like to override configurations from your toml config: + + +```scheme +# init.scm + +(require "helix/configuration.scm") + +(file-picker (fp-hidden #f)) +(cursorline #t) +(soft-wrap (sw-enable #t)) + +``` + + +### keymaps.scm + +Applying custom keybindings for certain file extensions: + + +```scheme +# init.scm + +(require "cogs/keymaps.scm") +(require (only-in "cogs/file-tree.scm" FILE-TREE-KEYBINDINGS FILE-TREE)) +(require (only-in "cogs/recentf.scm" recentf-open-files get-recent-files recentf-snapshot)) + +;; Set the global keybinding for now +(add-global-keybinding (hash "normal" (hash "C-r" (hash "f" ":recentf-open-files")))) + +(define scm-keybindings (hash "insert" (hash "ret" ':scheme-indent "C-l" ':insert-lambda))) + +;; Grab whatever the existing keybinding map is +(define standard-keybindings (deep-copy-global-keybindings)) + +(define file-tree-base (deep-copy-global-keybindings)) + +(merge-keybindings standard-keybindings scm-keybindings) +(merge-keybindings file-tree-base FILE-TREE-KEYBINDINGS) + +(set-global-buffer-or-extension-keymap (hash "scm" standard-keybindings FILE-TREE file-tree-base)) + +``` + +In insert mode, this overrides the `ret` keybinding to instead use a custom scheme indent function. Functions _must_ be available as typed commands, and are referred to +as symbols. So in this case, the `scheme-indent` function was exported by my `helix.scm` module. diff --git a/flake.nix b/flake.nix index c7e4fdce5f9e..a112133a0533 100644 --- a/flake.nix +++ b/flake.nix @@ -74,6 +74,25 @@ # filter out unnecessary paths filter = ignorePaths; }; + + helix-cogs = craneLibStable.buildPackage (commonArgs // { + pname = "helix-cogs"; + version = "0.1.0"; + cargoArtifacts = craneLibStable.buildDepsOnly commonArgs; + + buildPhase = '' + export HOME=$PWD/build_home # code-gen will write files relative to $HOME + cargoBuildLog=$(mktemp cargoBuildLogXXXX.json) + cargo run --package xtask -- code-gen --message-format json-render-diagnostics >"$cargoBuildLog" + ''; + + postInstall = '' + mkdir -p $out/cogs + cp -r build_home/.config/helix/* "$out/cogs" + ''; + + }); + makeOverridableHelix = old: config: let grammars = pkgs.callPackage ./grammars.nix config; runtimeDir = pkgs.runCommand "helix-runtime" {} '' @@ -146,6 +165,7 @@ ''; }); helix = makeOverridableHelix self.packages.${system}.helix-unwrapped {}; + helix-cogs = helix-cogs; default = self.packages.${system}.helix; }; @@ -178,7 +198,8 @@ [lld_13 cargo-flamegraph rust-analyzer] ++ (lib.optional (stdenv.isx86_64 && stdenv.isLinux) pkgs.cargo-tarpaulin) ++ (lib.optional stdenv.isLinux pkgs.lldb) - ++ (lib.optional stdenv.isDarwin pkgs.darwin.apple_sdk.frameworks.CoreFoundation); + ++ (lib.optional stdenv.isDarwin (with pkgs.darwin.apple_sdk.frameworks; + [CoreFoundation Security])); shellHook = '' export HELIX_RUNTIME="$PWD/runtime" export RUST_BACKTRACE="1" diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 4cd516268b0d..6118458080b3 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -14,6 +14,7 @@ homepage.workspace = true [features] unicode-lines = ["ropey/unicode_lines"] integration = [] +steel = ["dep:steel-core"] [dependencies] helix-stdx = { path = "../helix-stdx" } @@ -55,6 +56,7 @@ chrono = { version = "0.4", default-features = false, features = ["alloc", "std" etcetera = "0.8" textwrap = "0.16.1" +steel-core = { workspace = true, optional = true } nucleo.workspace = true parking_lot = "0.12" globset = "0.4.15" diff --git a/helix-core/src/extensions.rs b/helix-core/src/extensions.rs new file mode 100644 index 000000000000..6cdbda4e9fb3 --- /dev/null +++ b/helix-core/src/extensions.rs @@ -0,0 +1,263 @@ +#[cfg(feature = "steel")] +pub mod steel_implementations { + + use std::borrow::Cow; + + use steel::{ + gc::ShareableMut, + rvals::{as_underlying_type, Custom, SteelString}, + steel_vm::{builtin::BuiltInModule, register_fn::RegisterFn}, + SteelVal, + }; + + use helix_stdx::rope::RopeSliceExt; + + use crate::syntax::{AutoPairConfig, SoftWrap}; + + impl steel::rvals::Custom for crate::Position {} + impl steel::rvals::Custom for crate::Selection {} + impl steel::rvals::Custom for AutoPairConfig {} + impl steel::rvals::Custom for SoftWrap {} + + pub struct RopeyError(ropey::Error); + + impl steel::rvals::Custom for RopeyError {} + + impl From for RopeyError { + fn from(value: ropey::Error) -> Self { + Self(value) + } + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + enum RangeKind { + Char, + Byte, + } + + #[derive(Clone, PartialEq, Eq)] + pub struct SteelRopeSlice { + text: crate::Rope, + start: usize, + end: usize, + kind: RangeKind, + } + + impl Custom for SteelRopeSlice { + // `equal?` on two ropes should return true if they are the same + fn equality_hint(&self, other: &dyn steel::rvals::CustomType) -> bool { + if let Some(other) = as_underlying_type::(other) { + self == other + } else { + false + } + } + + fn equality_hint_general(&self, other: &steel::SteelVal) -> bool { + match other { + SteelVal::StringV(s) => self.to_slice() == s.as_str(), + SteelVal::Custom(c) => Self::equality_hint(&self, c.read().as_ref()), + + _ => false, + } + } + } + + impl SteelRopeSlice { + pub fn from_string(string: SteelString) -> Self { + Self { + text: crate::Rope::from_str(string.as_str()), + start: 0, + end: string.len(), + kind: RangeKind::Char, + } + } + + pub fn new(rope: crate::Rope) -> Self { + let end = rope.len_chars(); + Self { + text: rope, + start: 0, + end, + kind: RangeKind::Char, + } + } + + fn to_slice(&self) -> crate::RopeSlice<'_> { + match self.kind { + RangeKind::Char => self.text.slice(self.start..self.end), + RangeKind::Byte => self.text.byte_slice(self.start..self.end), + } + } + + pub fn line(mut self, cursor: usize) -> Result { + match self.kind { + RangeKind::Char => { + let slice = self.text.get_slice(self.start..self.end).ok_or_else(|| { + RopeyError(ropey::Error::CharIndexOutOfBounds(self.start, self.end)) + })?; + + // Move the start range, to wherever this lines up + let index = slice.try_line_to_char(cursor)?; + + let line = slice.line(cursor); + + self.start += index; + self.end = self.start + line.len_chars(); + + Ok(self) + } + RangeKind::Byte => { + let slice = + self.text + .get_byte_slice(self.start..self.end) + .ok_or_else(|| { + RopeyError(ropey::Error::ByteIndexOutOfBounds(self.start, self.end)) + })?; + + // Move the start range, to wherever this lines up + let index = slice.try_line_to_byte(cursor)?; + let line = slice.line(cursor); + + self.start += index; + self.end = self.start + line.len_bytes(); + + Ok(self) + } + } + } + + pub fn slice(mut self, lower: usize, upper: usize) -> Result { + match self.kind { + RangeKind::Char => { + self.end = self.start + upper; + self.start += lower; + + // Just check that this is legal + self.text.get_slice(self.start..self.end).ok_or_else(|| { + RopeyError(ropey::Error::CharIndexOutOfBounds(self.start, self.end)) + })?; + + Ok(self) + } + RangeKind::Byte => { + self.start = self.text.try_byte_to_char(self.start)? + lower; + self.end = self.start + (upper - lower); + + self.text + .get_byte_slice(self.start..self.end) + .ok_or_else(|| { + RopeyError(ropey::Error::ByteIndexOutOfBounds(self.start, self.end)) + })?; + + self.kind = RangeKind::Char; + Ok(self) + } + } + } + + pub fn byte_slice(mut self, lower: usize, upper: usize) -> Result { + match self.kind { + RangeKind::Char => { + self.start = self.text.try_char_to_byte(self.start)? + lower; + self.end = self.start + (upper - lower); + self.kind = RangeKind::Byte; + + // Just check that this is legal + self.text.get_slice(self.start..self.end).ok_or_else(|| { + RopeyError(ropey::Error::CharIndexOutOfBounds(self.start, self.end)) + })?; + + Ok(self) + } + RangeKind::Byte => { + self.start += lower; + self.end = self.start + (upper - lower); + + self.text + .get_byte_slice(self.start..self.end) + .ok_or_else(|| { + RopeyError(ropey::Error::ByteIndexOutOfBounds(self.start, self.end)) + })?; + + Ok(self) + } + } + } + + pub fn char_to_byte(&self, pos: usize) -> Result { + Ok(self.to_slice().try_char_to_byte(pos)?) + } + + pub fn to_string(&self) -> String { + self.to_slice().to_string() + } + + pub fn len_chars(&self) -> usize { + self.to_slice().len_chars() + } + + pub fn get_char(&self, index: usize) -> Option { + self.to_slice().get_char(index) + } + + pub fn len_lines(&self) -> usize { + self.to_slice().len_lines() + } + + pub fn trim_start(mut self) -> Self { + let slice = self.to_slice(); + + for (idx, c) in slice.chars().enumerate() { + if !c.is_whitespace() { + match self.kind { + RangeKind::Char => { + self.start += idx; + } + RangeKind::Byte => { + self.start += slice.char_to_byte(idx); + } + } + + break; + } + } + + self + } + + pub fn trimmed_starts_with(&self, pat: SteelString) -> bool { + let maybe_owned = Cow::from(self.to_slice()); + + maybe_owned.trim_start().starts_with(pat.as_str()) + } + + pub fn starts_with(&self, pat: SteelString) -> bool { + self.to_slice().starts_with(pat.as_str()) + } + + pub fn ends_with(&self, pat: SteelString) -> bool { + self.to_slice().ends_with(pat.as_str()) + } + } + + pub fn rope_module() -> BuiltInModule { + let mut module = BuiltInModule::new("helix/core/text"); + + module + .register_fn("string->rope", SteelRopeSlice::from_string) + .register_fn("rope->slice", SteelRopeSlice::slice) + .register_fn("rope-char->byte", SteelRopeSlice::char_to_byte) + .register_fn("rope->byte-slice", SteelRopeSlice::byte_slice) + .register_fn("rope->line", SteelRopeSlice::line) + .register_fn("rope->string", SteelRopeSlice::to_string) + .register_fn("rope-len-chars", SteelRopeSlice::len_chars) + .register_fn("rope-char-ref", SteelRopeSlice::get_char) + .register_fn("rope-len-lines", SteelRopeSlice::len_lines) + .register_fn("rope-starts-with?", SteelRopeSlice::starts_with) + .register_fn("rope-ends-with?", SteelRopeSlice::ends_with) + .register_fn("rope-trim-start", SteelRopeSlice::trim_start); + + module + } +} diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 9165560d0aa5..73f3314af730 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -30,6 +30,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-event/Cargo.toml b/helix-event/Cargo.toml index 87e5019bd553..874a38f21e31 100644 --- a/helix-event/Cargo.toml +++ b/helix-event/Cargo.toml @@ -16,8 +16,8 @@ ahash = "0.8.11" hashbrown = "0.14.5" tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] } # the event registry is essentially read only but must be an rwlock so we can -# setup new events on initialization, hardware-lock-elision hugely benefits this case -# as it essentially makes the lock entirely free as long as there is no writes +# setup new events on intalization, hardware-lock-elision hugnly benefits this case +# as is essentially makes the lock entirely free as long as there is no writes parking_lot = { version = "0.12", features = ["hardware-lock-elision"] } once_cell = "1.20" diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index cc1c4ce8fe67..c5145e01bcf3 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -1548,4 +1548,63 @@ impl Client { changes, }) } + + // Everything below is explicitly extensions used for handling non standard lsp commands + pub fn non_standard_extension( + &self, + method_name: String, + params: Option, + ) -> Option>> { + Some(self.call_non_standard(DynamicLspRequest { + method_name, + params, + })) + } + + fn call_non_standard(&self, request: DynamicLspRequest) -> impl Future> { + self.call_non_standard_with_timeout(request, self.req_timeout) + } + + fn call_non_standard_with_timeout( + &self, + request: DynamicLspRequest, + timeout_secs: u64, + ) -> impl Future> { + let server_tx = self.server_tx.clone(); + let id = self.next_request_id(); + + let params = serde_json::to_value(&request.params); + async move { + use std::time::Duration; + use tokio::time::timeout; + + let request = jsonrpc::MethodCall { + jsonrpc: Some(jsonrpc::Version::V2), + id: id.clone(), + method: (&request.method_name).to_string(), + params: Self::value_into_params(params?), + }; + + let (tx, mut rx) = channel::>(1); + + server_tx + .send(Payload::Request { + chan: tx, + value: request, + }) + .map_err(|e| Error::Other(e.into()))?; + + // TODO: delay other calls until initialize success + timeout(Duration::from_secs(timeout_secs), rx.recv()) + .await + .map_err(|_| Error::Timeout(id))? // return Timeout + .ok_or(Error::StreamClosed)? + } + } +} + +#[derive(serde::Serialize, Deserialize)] +pub struct DynamicLspRequest { + method_name: String, + params: Option, } diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index b14d3d3c5211..9d5979796cf4 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -13,10 +13,11 @@ repository.workspace = true homepage.workspace = true [features] -default = ["git"] +default = ["git", "steel"] # Remove steel if you don't want it unicode-lines = ["helix-core/unicode-lines", "helix-view/unicode-lines"] integration = ["helix-event/integration_test"] git = ["helix-vcs/git"] +steel = ["dep:steel-core", "helix-core/steel", "helix-view/steel", "tui/steel"] [[bin]] name = "hx" @@ -72,6 +73,11 @@ serde = { version = "1.0", features = ["derive"] } grep-regex = "0.1.13" grep-searcher = "0.1.14" +# plugin support +steel-core = { workspace = true, optional = true } +steel-doc = { git = "https://github.com/mattwparas/steel.git", version = "0.6.0" } +# steel-doc = { path = "/Users/matt/code/steel/crates/steel-doc", version = "0.6.0" } + [target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100 signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } libc = "0.2.159" diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index a567815fcaa6..f92082685b8e 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -248,7 +248,7 @@ impl Application { ]) .context("build signal handler")?; - let app = Self { + let mut app = Self { compositor, terminal, editor, @@ -263,6 +263,22 @@ impl Application { lsp_progress: LspProgressMap::new(), }; + { + let mut cx = crate::commands::Context { + register: None, + count: std::num::NonZeroUsize::new(1), + editor: &mut app.editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs: &mut app.jobs, + }; + + crate::commands::ScriptingEngine::run_initialization_script( + &mut cx, + app.config.clone(), + ); + } + Ok(app) } @@ -353,6 +369,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; @@ -389,6 +409,7 @@ impl Application { }; self.config.store(Arc::new(app_config)); } + ConfigEvent::Change => {} } // Update all the relevant members in the editor after updating diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index b1c29378dec6..9bb1fb6d3296 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1,4 +1,5 @@ pub(crate) mod dap; +pub(crate) mod engine; pub(crate) mod lsp; pub(crate) mod typed; @@ -11,7 +12,16 @@ use helix_stdx::{ }; use helix_vcs::{FileChange, Hunk}; pub use lsp::*; -use tui::text::Span; + +pub use engine::ScriptingEngine; + +#[cfg(feature = "steel")] +pub use engine::steel::{helix_module_file, steel_init_file}; + +use tui::{ + text::Span, + widgets::{Cell, Row}, +}; pub use typed::*; use helix_core::{ @@ -226,6 +236,7 @@ impl MappableCommand { match &self { Self::Typable { name, args, doc: _ } => { let args: Vec> = args.iter().map(Cow::from).collect(); + // TODO: Swap the order to allow overriding the existing commands? if let Some(command) = typed::TYPABLE_COMMAND_MAP.get(name.as_str()) { let mut cx = compositor::Context { editor: cx.editor, @@ -235,6 +246,8 @@ impl MappableCommand { if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) { cx.editor.set_error(format!("{}", e)); } + } else { + ScriptingEngine::call_function_by_name(cx, name, args); } } Self::Static { fun, .. } => (fun)(cx), @@ -274,6 +287,14 @@ impl MappableCommand { } } + pub(crate) fn doc_mut(&mut self) -> Option<&mut String> { + if let Self::Typable { doc, .. } = self { + Some(doc) + } else { + None + } + } + #[rustfmt::skip] static_commands!( no_op, "Do nothing", @@ -613,7 +634,14 @@ impl std::str::FromStr for MappableCommand { .map(|cmd| MappableCommand::Typable { name: cmd.name.to_owned(), doc: format!(":{} {:?}", cmd.name, args), - args, + args: args.clone(), + }) + .or_else(|| { + Some(MappableCommand::Typable { + name: name.to_owned(), + args, + doc: "Undocumented plugin command".to_string(), + }) }) .ok_or_else(|| anyhow!("No TypableCommand named '{}'", s)) } else if let Some(suffix) = s.strip_prefix('@') { @@ -2954,6 +2982,7 @@ fn buffer_picker(cx: &mut Context) { struct BufferMeta { id: DocumentId, path: Option, + name: Option, is_modified: bool, is_current: bool, focused_at: std::time::Instant, @@ -2962,6 +2991,7 @@ fn buffer_picker(cx: &mut Context) { let new_meta = |doc: &Document| BufferMeta { id: doc.id(), path: doc.path().cloned(), + name: doc.name.clone(), is_modified: doc.is_modified(), is_current: doc.id() == current, focused_at: doc.focused_at, @@ -3861,6 +3891,18 @@ pub mod insert { helix_event::dispatch(PostInsertChar { c, cx }); } + pub fn insert_string(cx: &mut Context, string: String) { + let (view, doc) = current!(cx.editor); + + let indent = Tendril::from(string); + let transaction = Transaction::insert( + doc.text(), + &doc.selection(view.id).clone().cursors(doc.text().slice(..)), + indent, + ); + doc.apply(&transaction, view.id); + } + pub fn smart_tab(cx: &mut Context) { let (view, doc) = current_ref!(cx.editor); let view_id = view.id; diff --git a/helix-term/src/commands/engine.rs b/helix-term/src/commands/engine.rs new file mode 100644 index 000000000000..5c7528458da9 --- /dev/null +++ b/helix-term/src/commands/engine.rs @@ -0,0 +1,213 @@ +use arc_swap::ArcSwapAny; +use helix_view::{document::Mode, input::KeyEvent}; + +use std::{borrow::Cow, sync::Arc}; + +use crate::{ + compositor, + config::Config, + keymap::KeymapResult, + ui::{self, PromptEvent}, +}; + +use super::{Context, MappableCommand, TYPABLE_COMMAND_LIST}; + +#[cfg(feature = "steel")] +mod components; + +#[cfg(feature = "steel")] +pub mod steel; + +pub enum PluginSystemKind { + None, + #[cfg(feature = "steel")] + Steel, +} + +pub enum PluginSystemTypes { + None(NoEngine), + #[cfg(feature = "steel")] + Steel(steel::SteelScriptingEngine), +} + +// The order in which the plugins will be evaluated against - if we wanted to include, lets say `rhai`, +// we would have to order the precedence for searching for exported commands, or somehow merge them? +const PLUGIN_PRECEDENCE: &[PluginSystemTypes] = &[ + #[cfg(feature = "steel")] + PluginSystemTypes::Steel(steel::SteelScriptingEngine), + PluginSystemTypes::None(NoEngine), +]; + +pub struct NoEngine; + +// This will be the boundary layer between the editor and the engine. +pub struct ScriptingEngine; + +// Macro to automatically dispatch to hopefully get some inlining +macro_rules! manual_dispatch { + ($kind:expr, $raw:tt ($($args:expr),* $(,)?) ) => { + match $kind { + PluginSystemTypes::None(n) => n.$raw($($args),*), + #[cfg(feature = "steel")] + PluginSystemTypes::Steel(s) => s.$raw($($args),*), + } + }; +} + +impl ScriptingEngine { + pub fn initialize() { + for kind in PLUGIN_PRECEDENCE { + manual_dispatch!(kind, initialize()) + } + } + + pub fn run_initialization_script( + cx: &mut Context, + configuration: Arc>>, + ) { + for kind in PLUGIN_PRECEDENCE { + manual_dispatch!(kind, run_initialization_script(cx, configuration.clone())) + } + } + + pub fn handle_keymap_event( + editor: &mut ui::EditorView, + mode: Mode, + cxt: &mut Context, + event: KeyEvent, + ) -> Option { + for kind in PLUGIN_PRECEDENCE { + let res = manual_dispatch!(kind, handle_keymap_event(editor, mode, cxt, event)); + + if res.is_some() { + return res; + } + } + + None + } + + pub fn call_function_by_name(cx: &mut Context, name: &str, args: Vec>) -> bool { + for kind in PLUGIN_PRECEDENCE { + if manual_dispatch!(kind, call_function_by_name(cx, name, &args)) { + return true; + } + } + + false + } + + pub fn call_typed_command<'a>( + cx: &mut compositor::Context, + input: &'a str, + parts: &'a [&'a str], + event: PromptEvent, + ) -> bool { + for kind in PLUGIN_PRECEDENCE { + if manual_dispatch!(kind, call_typed_command(cx, input, parts, event)) { + return true; + } + } + + false + } + + pub fn get_doc_for_identifier(ident: &str) -> Option { + for kind in PLUGIN_PRECEDENCE { + let doc = manual_dispatch!(kind, get_doc_for_identifier(ident)); + + if doc.is_some() { + return doc; + } + } + + None + } + + pub fn available_commands<'a>() -> Vec> { + PLUGIN_PRECEDENCE + .iter() + .flat_map(|kind| manual_dispatch!(kind, available_commands())) + .collect() + } + + pub fn generate_sources() { + for kind in PLUGIN_PRECEDENCE { + manual_dispatch!(kind, generate_sources()) + } + } +} + +impl PluginSystem for NoEngine { + fn engine_name(&self) -> PluginSystemKind { + PluginSystemKind::None + } +} + +/// These methods are the main entry point for interaction with the rest of +/// the editor system. +pub trait PluginSystem { + /// If any initialization needs to happen prior to the initialization script being run, + /// this is done here. This is run before the context is available. + fn initialize(&self) {} + + fn engine_name(&self) -> PluginSystemKind; + + /// Post initialization, once the context is available. This means you should be able to + /// run anything here that could modify the context before the main editor is available. + fn run_initialization_script( + &self, + _cx: &mut Context, + _configuration: Arc>>, + ) { + } + + /// Allow the engine to directly handle a keymap event. This is some of the tightest integration + /// with the engine, directly intercepting any keymap events. By default, this just delegates to the + /// editors default keybindings. + #[inline(always)] + fn handle_keymap_event( + &self, + _editor: &mut ui::EditorView, + _mode: Mode, + _cxt: &mut Context, + _event: KeyEvent, + ) -> Option { + None + } + + /// This attempts to call a function in the engine with the name `name` using the args `args`. The context + /// is available here. Returns a bool indicating whether the function exists or not. + #[inline(always)] + fn call_function_by_name(&self, _cx: &mut Context, _name: &str, _args: &[Cow]) -> bool { + false + } + + /// This is explicitly for calling a function via the typed command interface, e.g. `:vsplit`. The context here + /// that is available is more limited than the context available in `call_function_if_global_exists`. This also + /// gives the ability to handle in progress commands with `PromptEvent`. + #[inline(always)] + fn call_typed_command<'a>( + &self, + _cx: &mut compositor::Context, + _input: &'a str, + _parts: &'a [&'a str], + _event: PromptEvent, + ) -> bool { + false + } + + /// Given an identifier, extract the documentation from the engine. + #[inline(always)] + fn get_doc_for_identifier(&self, _ident: &str) -> Option { + None + } + + /// Fuzzy match the input against the fuzzy matcher, used for handling completions on typed commands + #[inline(always)] + fn available_commands<'a>(&self) -> Vec> { + Vec::new() + } + + fn generate_sources(&self) {} +} diff --git a/helix-term/src/commands/engine/components.rs b/helix-term/src/commands/engine/components.rs new file mode 100644 index 000000000000..fb11f16a991b --- /dev/null +++ b/helix-term/src/commands/engine/components.rs @@ -0,0 +1,855 @@ +use std::{collections::HashMap, sync::Arc}; + +use helix_core::Position; +use helix_view::{ + graphics::{Color, CursorKind, Rect, UnderlineStyle}, + input::{Event, KeyEvent, MouseButton, MouseEvent}, + keyboard::{KeyCode, KeyModifiers}, + theme::{Modifier, Style}, + Editor, +}; +use steel::{ + rvals::{as_underlying_type, Custom, FromSteelVal, IntoSteelVal, SteelString}, + steel_vm::{builtin::BuiltInModule, engine::Engine, register_fn::RegisterFn}, + SteelVal, +}; +use tokio::sync::Mutex; +use tui::{ + buffer::Buffer, + text::Text, + widgets::{self, Block, BorderType, Borders, ListItem, Widget}, +}; + +use crate::{ + commands::{engine::steel::BoxDynComponent, Context}, + compositor::{self, Component}, + ui::overlay::overlaid, +}; + +use super::steel::{enter_engine, present_error_inside_engine_context, WrappedDynComponent}; + +#[derive(Clone)] +struct AsyncReader { + // Take that, and write it back to a terminal session that is + // getting rendered. + channel: Arc>>, +} + +impl AsyncReader { + async fn read_line(self) -> Option { + let mut buf = String::new(); + + let mut guard = self.channel.lock().await; + + while let Ok(v) = guard.try_recv() { + buf.push_str(&v); + } + + let fut = guard.recv(); + + // If we haven't found any characters, just wait until we have something. + // Otherwise, we give this a 2 ms buffer to check if more things are + // coming through the pipe. + if buf.is_empty() { + let next = fut.await; + + match next { + Some(v) => { + buf.push_str(&v); + Some(buf) + } + None => None, + } + } else { + match tokio::time::timeout(std::time::Duration::from_millis(2), fut).await { + Ok(Some(v)) => { + buf.push_str(&v); + Some(buf) + } + Ok(None) => { + if buf.is_empty() { + None + } else { + Some(buf) + } + } + Err(_) => Some(buf), + } + } + } +} + +impl Custom for AsyncReader {} + +struct AsyncWriter { + channel: tokio::sync::mpsc::UnboundedSender, +} + +impl std::io::Write for AsyncWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + if let Err(_) = self.channel.send(String::from_utf8_lossy(buf).to_string()) { + Ok(0) + } else { + Ok(buf.len()) + } + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +// TODO: Move the main configuration function to use this instead +pub fn helix_component_module() -> BuiltInModule { + let mut module = BuiltInModule::new("helix/components"); + + module + .register_fn("async-read-line", AsyncReader::read_line) + // TODO: + .register_fn("make-async-reader-writer", || { + let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); + + let writer = AsyncWriter { channel: sender }; + let reader = AsyncReader { + channel: Arc::new(Mutex::new(receiver)), + }; + + vec![ + SteelVal::new_dyn_writer_port(writer), + reader.into_steelval().unwrap(), + ] + }) + // Attempt to pop off a specific component + .register_fn( + "pop-dynamic-component-by-name", + |ctx: &mut Context, name: SteelString| { + // Removing a component by name here will be important! + todo!() + }, + ) + .register_fn("theme->bg", |ctx: &mut Context| { + ctx.editor.theme.get("ui.background") + }) + .register_fn("theme->fg", |ctx: &mut Context| { + ctx.editor.theme.get("ui.text") + }) + .register_fn("buffer-area", |buffer: &mut Buffer| buffer.area) + .register_fn("frame-set-string!", buffer_set_string) + .register_fn("new-component!", SteelDynamicComponent::new_dyn) + .register_fn("position", Position::new) + .register_fn("position-row", |position: &Position| position.row) + .register_fn("position-col", |position: &Position| position.col) + .register_fn( + "set-position-row!", + |position: &mut Position, row: usize| { + position.row = row; + }, + ) + .register_fn( + "set-position-col!", + |position: &mut Position, col: usize| { + position.col = col; + }, + ) + .register_fn("area", helix_view::graphics::Rect::new) + .register_fn("area-x", |area: &helix_view::graphics::Rect| area.x) + .register_fn("area-y", |area: &helix_view::graphics::Rect| area.y) + .register_fn("area-width", |area: &helix_view::graphics::Rect| area.width) + .register_fn("area-height", |area: &helix_view::graphics::Rect| { + area.height + }) + .register_fn("overlaid", |component: &mut WrappedDynComponent| { + let inner: Option> = + component.inner.take().map(|x| { + Box::new(overlaid(BoxDynComponent::new(x))) + as Box + }); + + component.inner = inner; + }) + .register_fn("widget/list", |items: Vec| { + widgets::List::new( + items + .into_iter() + .map(|x| ListItem::new(Text::from(x))) + .collect::>(), + ) + }) + // Pass references in as well? + .register_fn( + "widget/list/render", + |buf: &mut Buffer, area: Rect, list: widgets::List| list.render(area, buf), + ) + .register_fn("block", || { + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::White)) + .border_type(BorderType::Rounded) + .style(Style::default().bg(Color::Black)) + }) + // TODO: Expose these accordingly + .register_fn( + "make-block", + |style: Style, border_style: Style, borders: SteelString, border_type: SteelString| { + let border_type = match border_type.as_str() { + "plain" => BorderType::Plain, + "rounded" => BorderType::Rounded, + "double" => BorderType::Double, + "thick" => BorderType::Thick, + _ => BorderType::Plain, + }; + + let borders = match borders.as_str() { + "top" => Borders::TOP, + "left" => Borders::LEFT, + "right" => Borders::RIGHT, + "bottom" => Borders::BOTTOM, + "all" => Borders::ALL, + _ => Borders::empty(), + }; + + Block::default() + .borders(borders) + .border_style(border_style) + .border_type(border_type) + .style(style) + }, + ) + .register_fn( + "block/render", + |buf: &mut Buffer, area: Rect, block: Block| block.render(area, buf), + ) + .register_fn("buffer/clear", Buffer::clear) + .register_fn("buffer/clear-with", Buffer::clear_with) + // Mutate a color in place, to save some headache. + .register_fn( + "set-color-rgb!", + |color: &mut Color, r: u8, g: u8, b: u8| { + *color = Color::Rgb(r, g, b); + }, + ) + .register_fn("set-color-indexed!", |color: &mut Color, index: u8| { + *color = Color::Indexed(index); + }) + .register_value("Color/Reset", Color::Reset.into_steelval().unwrap()) + .register_value("Color/Black", Color::Black.into_steelval().unwrap()) + .register_value("Color/Red", Color::Red.into_steelval().unwrap()) + .register_value("Color/White", Color::White.into_steelval().unwrap()) + .register_value("Color/Green", Color::Green.into_steelval().unwrap()) + .register_value("Color/Yellow", Color::Yellow.into_steelval().unwrap()) + .register_value("Color/Blue", Color::Blue.into_steelval().unwrap()) + .register_value("Color/Magenta", Color::Magenta.into_steelval().unwrap()) + .register_value("Color/Cyan", Color::Cyan.into_steelval().unwrap()) + .register_value("Color/Gray", Color::Gray.into_steelval().unwrap()) + .register_value("Color/LightRed", Color::LightRed.into_steelval().unwrap()) + .register_value( + "Color/LightGreen", + Color::LightGreen.into_steelval().unwrap(), + ) + .register_value( + "Color/LightYellow", + Color::LightYellow.into_steelval().unwrap(), + ) + .register_value("Color/LightBlue", Color::LightBlue.into_steelval().unwrap()) + .register_value( + "Color/LightMagenta", + Color::LightMagenta.into_steelval().unwrap(), + ) + .register_value("Color/LightCyan", Color::LightCyan.into_steelval().unwrap()) + .register_value("Color/LightGray", Color::LightGray.into_steelval().unwrap()) + .register_fn("Color/rgb", Color::Rgb) + .register_fn("Color-red", Color::red) + .register_fn("Color-green", Color::green) + .register_fn("Color-blue", Color::blue) + .register_fn("Color/Indexed", Color::Indexed) + .register_fn("set-style-fg!", |style: &mut Style, color: Color| { + style.fg = Some(color); + }) + .register_fn("style-fg", Style::fg) + .register_fn("style-bg", Style::bg) + .register_fn("style-with-italics", |style: &Style| { + let patch = Style::default().add_modifier(Modifier::ITALIC); + style.patch(patch) + }) + .register_fn("style-with-bold", |style: Style| { + let patch = Style::default().add_modifier(Modifier::BOLD); + style.patch(patch) + }) + .register_fn("style-with-dim", |style: &Style| { + let patch = Style::default().add_modifier(Modifier::DIM); + style.patch(patch) + }) + .register_fn("style-with-slow-blink", |style: Style| { + let patch = Style::default().add_modifier(Modifier::SLOW_BLINK); + style.patch(patch) + }) + .register_fn("style-with-rapid-blink", |style: Style| { + let patch = Style::default().add_modifier(Modifier::RAPID_BLINK); + style.patch(patch) + }) + .register_fn("style-with-reversed", |style: Style| { + let patch = Style::default().add_modifier(Modifier::REVERSED); + style.patch(patch) + }) + .register_fn("style-with-hidden", |style: Style| { + let patch = Style::default().add_modifier(Modifier::HIDDEN); + style.patch(patch) + }) + .register_fn("style-with-crossed-out", |style: Style| { + let patch = Style::default().add_modifier(Modifier::CROSSED_OUT); + style.patch(patch) + }) + .register_fn("style->fg", |style: &Style| style.fg) + .register_fn("style->bg", |style: &Style| style.bg) + .register_fn("set-style-bg!", |style: &mut Style, color: Color| { + style.bg = Some(color); + }) + .register_fn("style-underline-color", Style::underline_color) + .register_fn("style-underline-style", Style::underline_style) + .register_value( + "Underline/Reset", + UnderlineStyle::Reset.into_steelval().unwrap(), + ) + .register_value( + "Underline/Line", + UnderlineStyle::Line.into_steelval().unwrap(), + ) + .register_value( + "Underline/Curl", + UnderlineStyle::Curl.into_steelval().unwrap(), + ) + .register_value( + "Underline/Dotted", + UnderlineStyle::Dotted.into_steelval().unwrap(), + ) + .register_value( + "Underline/Dashed", + UnderlineStyle::Dashed.into_steelval().unwrap(), + ) + .register_value( + "Underline/DoubleLine", + UnderlineStyle::DoubleLine.into_steelval().unwrap(), + ) + .register_fn("style", || Style::default()) + .register_value( + "event-result/consume", + SteelEventResult::Consumed.into_steelval().unwrap(), + ) + .register_value( + "event-result/consume-without-rerender", + SteelEventResult::ConsumedWithoutRerender + .into_steelval() + .unwrap(), + ) + .register_value( + "event-result/ignore", + SteelEventResult::Ignored.into_steelval().unwrap(), + ) + .register_value( + "event-result/close", + SteelEventResult::Close.into_steelval().unwrap(), + ) + // TODO: Use a reference here instead of passing by value. + .register_fn("key-event-char", |event: Event| { + if let Event::Key(event) = event { + event.char() + } else { + None + } + }) + .register_fn("key-event-modifier", |event: Event| { + if let Event::Key(KeyEvent { modifiers, .. }) = event { + Some(modifiers.bits()) + } else { + None + } + }) + .register_value( + "key-modifier-ctrl", + SteelVal::IntV(KeyModifiers::CONTROL.bits() as isize), + ) + .register_value( + "key-modifier-shift", + SteelVal::IntV(KeyModifiers::SHIFT.bits() as isize), + ) + .register_value( + "key-modifier-alt", + SteelVal::IntV(KeyModifiers::ALT.bits() as isize), + ) + .register_fn("key-event-F?", |event: Event, number: u8| match event { + Event::Key(KeyEvent { + code: KeyCode::F(x), + .. + }) if number == x => true, + _ => false, + }) + .register_fn("mouse-event?", |event: Event| { + matches!(event, Event::Mouse(_)) + }) + .register_fn("event-mouse-kind", |event: Event| { + if let Event::Mouse(MouseEvent { kind, .. }) = event { + match kind { + helix_view::input::MouseEventKind::Down(MouseButton::Left) => 0, + helix_view::input::MouseEventKind::Down(MouseButton::Right) => 1, + helix_view::input::MouseEventKind::Down(MouseButton::Middle) => 2, + helix_view::input::MouseEventKind::Up(MouseButton::Left) => 3, + helix_view::input::MouseEventKind::Up(MouseButton::Right) => 4, + helix_view::input::MouseEventKind::Up(MouseButton::Middle) => 5, + helix_view::input::MouseEventKind::Drag(MouseButton::Left) => 6, + helix_view::input::MouseEventKind::Drag(MouseButton::Right) => 7, + helix_view::input::MouseEventKind::Drag(MouseButton::Middle) => 8, + helix_view::input::MouseEventKind::Moved => 9, + helix_view::input::MouseEventKind::ScrollDown => 10, + helix_view::input::MouseEventKind::ScrollUp => 11, + helix_view::input::MouseEventKind::ScrollLeft => 12, + helix_view::input::MouseEventKind::ScrollRight => 13, + } + .into_steelval() + } else { + false.into_steelval() + } + }) + .register_fn("event-mouse-row", |event: Event| { + if let Event::Mouse(MouseEvent { row, .. }) = event { + row.into_steelval() + } else { + false.into_steelval() + } + }) + .register_fn("event-mouse-col", |event: Event| { + if let Event::Mouse(MouseEvent { column, .. }) = event { + column.into_steelval() + } else { + false.into_steelval() + } + }) + // Is this mouse event within the area provided + .register_fn("mouse-event-within-area?", |event: Event, area: Rect| { + if let Event::Mouse(MouseEvent { row, column, .. }) = event { + column > area.x + && column < area.x + area.width + && row > area.y + && row < area.y + area.height + } else { + false + } + }); + + macro_rules! register_key_events { + ($ ( $name:expr => $key:tt ) , *, ) => { + $( + module.register_fn(concat!("key-event-", $name, "?"), |event: Event| { + matches!( + event, + Event::Key( + KeyEvent { + code: KeyCode::$key, + .. + } + )) + }); + )* + }; + } + + // Key events for individual key codes + register_key_events!( + "escape" => Esc, + "backspace" => Backspace, + "enter" => Enter, + "left" => Left, + "right" => Right, + "up" => Up, + "down" => Down, + "home" => Home, + "page-up" => PageUp, + "page-down" => PageDown, + "tab" => Tab, + "delete" => Delete, + "insert" => Insert, + "null" => Null, + "caps-lock" => CapsLock, + "scroll-lock" => ScrollLock, + "num-lock" => NumLock, + "print-screen" => PrintScreen, + "pause" => Pause, + "menu" => Menu, + "keypad-begin" => KeypadBegin, + ); + + module +} + +// fn buffer_set_string( +// buffer: &mut tui::buffer::Buffer, +// x: u16, +// y: u16, +// string: steel::rvals::SteelString, +// style: Style, +// ) { +// buffer.set_string(x, y, string.as_str(), style) +// } + +fn buffer_set_string( + buffer: &mut tui::buffer::Buffer, + x: u16, + y: u16, + string: SteelVal, + style: Style, +) -> steel::rvals::Result<()> { + match string { + SteelVal::StringV(string) => { + buffer.set_string(x, y, string.as_str(), style); + Ok(()) + } + SteelVal::Custom(c) => { + if let Some(string) = + as_underlying_type::(c.read().as_ref()) + { + buffer.set_string(x, y, string.string.as_str(), style); + Ok(()) + } else { + steel::stop!(TypeMismatch => "buffer-set-string! expected a string") + } + } + _ => { + steel::stop!(TypeMismatch => "buffer-set-string! expected a string") + } + } + + // buffer.set_string(x, y, string.as_str(), style) +} + +/// A dynamic component, used for rendering +#[derive(Clone)] +pub struct SteelDynamicComponent { + // TODO: currently the component id requires using a &'static str, + // however in a world with dynamic components that might not be + // the case anymore + name: String, + // This _should_ be a struct, but in theory can be whatever you want. It will be the first argument + // passed to the functions in the remainder of the struct. + state: SteelVal, + handle_event: Option, + should_update: Option, + render: SteelVal, + cursor: Option, + required_size: Option, + + // Cached key event; we keep this around so that when sending + // events to the event handler, we can reuse the heap allocation + // instead of re-allocating for every event (which might be a lot) + key_event: Option, +} + +impl SteelDynamicComponent { + pub fn new( + name: String, + state: SteelVal, + render: SteelVal, + h: HashMap, + ) -> Self { + Self { + name, + state, + render, + handle_event: h.get("handle_event").cloned(), + should_update: h.get("should_update").cloned(), + cursor: h.get("cursor").cloned(), + required_size: h.get("required_size").cloned(), + key_event: None, + } + } + + pub fn new_dyn( + name: String, + state: SteelVal, + render: SteelVal, + h: HashMap, + ) -> WrappedDynComponent { + let s = Self::new(name, state, render, h); + + // TODO: Add guards here for the + WrappedDynComponent { + inner: Some(Box::new(s)), + } + } + + pub fn get_state(&self) -> SteelVal { + self.state.clone() + } + + pub fn get_render(&self) -> SteelVal { + self.render.clone() + } + + pub fn get_handle_event(&self) -> Option { + self.handle_event.clone() + } + + pub fn get_should_update(&self) -> Option { + self.should_update.clone() + } + + pub fn get_cursor(&self) -> Option { + self.cursor.clone() + } + + pub fn get_required_size(&self) -> Option { + self.required_size.clone() + } +} + +impl Custom for SteelDynamicComponent {} + +impl Custom for Box {} + +#[derive(Clone)] +enum SteelEventResult { + Consumed, + Ignored, + Close, + ConsumedWithoutRerender, +} + +impl Custom for SteelEventResult {} + +impl Component for SteelDynamicComponent { + fn name(&self) -> Option<&str> { + Some(&self.name) + } + + fn render( + &mut self, + area: helix_view::graphics::Rect, + frame: &mut tui::buffer::Buffer, + ctx: &mut compositor::Context, + ) { + let mut ctx = Context { + register: None, + count: None, + editor: ctx.editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs: ctx.jobs, + }; + + // Pass the `state` object through - this can be used for storing the state of whatever plugin thing we're + // attempting to render + let thunk = |engine: &mut Engine, f| { + engine.call_function_with_args_from_mut_slice( + self.render.clone(), + &mut [self.state.clone(), area.into_steelval().unwrap(), f], + ) + }; + + enter_engine(|guard| { + if let Err(e) = guard + .with_mut_reference::(frame) + .with_mut_reference::(&mut ctx) + .consume(|engine, args| { + let mut arg_iter = args.into_iter(); + + let buffer = arg_iter.next().unwrap(); + let context = arg_iter.next().unwrap(); + + engine.update_value("*helix.cx*", context); + + (thunk)(engine, buffer) + }) + { + present_error_inside_engine_context(&mut ctx, guard, e) + } + }) + } + + // TODO: Pass in event as well? Need to have immutable reference type + // Otherwise, we're gonna be in a bad spot. For now - just clone the object and pass it through. + // Clong is _not_ ideal, but it might be all we can do for now. + fn handle_event( + &mut self, + event: &Event, + ctx: &mut compositor::Context, + ) -> compositor::EventResult { + if let Some(handle_event) = &mut self.handle_event { + let mut ctx = Context { + register: None, + count: None, + editor: ctx.editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs: ctx.jobs, + }; + + match self.key_event.as_mut() { + Some(SteelVal::Custom(key_event)) => { + // Save the headache, reuse the allocation + if let Some(inner) = + steel::rvals::as_underlying_type_mut::(key_event.write().as_mut()) + { + *inner = event.clone(); + } + } + + None => { + self.key_event = Some(event.clone().into_steelval().unwrap()); + } + _ => { + panic!("This event needs to stay as a steelval"); + } + } + + // Pass the `state` object through - this can be used for storing the state of whatever plugin thing we're + // attempting to render + let thunk = |engine: &mut Engine| { + engine.call_function_with_args_from_mut_slice( + handle_event.clone(), + &mut [self.state.clone(), self.key_event.clone().unwrap()], + ) + }; + + let close_fn = compositor::EventResult::Consumed(Some(Box::new( + |compositor: &mut compositor::Compositor, _| { + // remove the layer + compositor.pop(); + }, + ))); + + // let event = match event { + // Event::Key(event) => *event, + // _ => return compositor::EventResult::Ignored(None), + // }; + + match enter_engine(|guard| { + guard + .with_mut_reference::(&mut ctx) + .consume(move |engine, arguments| { + let context = arguments[0].clone(); + engine.update_value("*helix.cx*", context); + + thunk(engine) + }) + }) { + Ok(v) => { + let value = SteelEventResult::from_steelval(&v); + + match value { + Ok(SteelEventResult::Close) => close_fn, + Ok(SteelEventResult::Consumed) => compositor::EventResult::Consumed(None), + Ok(SteelEventResult::ConsumedWithoutRerender) => { + compositor::EventResult::ConsumedWithoutRerender + } + Ok(SteelEventResult::Ignored) => compositor::EventResult::Ignored(None), + _ => match event { + // ctrl!('c') | key!(Esc) => close_fn, + _ => compositor::EventResult::Ignored(None), + }, + } + } + Err(e) => { + // Present the error + enter_engine(|x| present_error_inside_engine_context(&mut ctx, x, e)); + + compositor::EventResult::Ignored(None) + } + } + } else { + compositor::EventResult::Ignored(None) + } + } + + fn should_update(&self) -> bool { + true + + // if let Some(should_update) = &self.should_update { + // match ENGINE.with(|x| { + // let res = x + // .borrow_mut() + // .call_function_with_args(should_update.clone(), vec![self.state.clone()]); + + // res + // }) { + // Ok(v) => bool::from_steelval(&v).unwrap_or(true), + // Err(_) => true, + // } + // } else { + // true + // } + } + + // TODO: Implement immutable references. Right now I'm only supporting mutable references. + fn cursor( + &self, + area: helix_view::graphics::Rect, + _ctx: &Editor, + ) -> ( + Option, + helix_view::graphics::CursorKind, + ) { + if let Some(cursor) = &self.cursor { + // Pass the `state` object through - this can be used for storing the state of whatever plugin thing we're + // attempting to render + let thunk = |engine: &mut Engine| { + engine.call_function_with_args_from_mut_slice( + cursor.clone(), + &mut [self.state.clone(), area.into_steelval().unwrap()], + ) + }; + + let result = + Option::::from_steelval(&enter_engine(|x| thunk(x).unwrap())); + + match result { + Ok(v) => (v, CursorKind::Block), + // TODO: Figure out how to pop up an error message + Err(_e) => { + log::info!("Error: {:?}", _e); + (None, CursorKind::Block) + } + } + } else { + (None, helix_view::graphics::CursorKind::Hidden) + } + } + + fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { + // let name = self.type_name(); + + if let Some(required_size) = &mut self.required_size { + // log::info!("Calling required-size inside: {}", name); + + // TODO: Create some token that we can grab to enqueue function calls internally. Referencing + // the external API would cause problems - we just need to include a handle to the interpreter + // instance. Something like: + // ENGINE.call_function_or_enqueue? OR - this is the externally facing render function. Internal + // render calls do _not_ go through this interface. Instead, they are just called directly. + // + // If we go through this interface, we're going to get an already borrowed mut error, since it is + // re-entrant attempting to grab the ENGINE instead mutably, since we have to break the recursion + // somehow. By putting it at the edge, we then say - hey for these functions on this interface, + // call the engine instance. Otherwise, all computation happens inside the engine. + match enter_engine(|x| { + x.call_function_with_args_from_mut_slice( + required_size.clone(), + &mut [self.state.clone(), viewport.into_steelval().unwrap()], + ) + }) + .and_then(|x| Option::<(u16, u16)>::from_steelval(&x)) + { + Ok(v) => v, + // TODO: Figure out how to present an error + Err(_e) => None, + } + } else { + None + } + } + + fn type_name(&self) -> &'static str { + std::any::type_name::() + } + + fn id(&self) -> Option<&'static str> { + None + } +} diff --git a/helix-term/src/commands/engine/steel.rs b/helix-term/src/commands/engine/steel.rs new file mode 100644 index 000000000000..ef1a7685769f --- /dev/null +++ b/helix-term/src/commands/engine/steel.rs @@ -0,0 +1,3220 @@ +use arc_swap::ArcSwapAny; +use crossterm::event::{Event, KeyCode, KeyModifiers}; +use helix_core::{ + diagnostic::Severity, + extensions::steel_implementations::{rope_module, SteelRopeSlice}, + find_workspace, graphemes, + shellwords::Shellwords, + syntax::{AutoPairConfig, SoftWrap}, + Range, Selection, Tendril, +}; +use helix_event::register_hook; +use helix_view::{ + annotations::diagnostics::DiagnosticFilter, + document::Mode, + editor::{ + Action, AutoSave, BufferLine, ConfigEvent, CursorShapeConfig, FilePickerConfig, + GutterConfig, IndentGuidesConfig, LineEndingConfig, LineNumber, LspConfig, SearchConfig, + SmartTabConfig, StatusLineConfig, TerminalConfig, WhitespaceConfig, + }, + extension::document_id_to_usize, + input::KeyEvent, + theme::Color, + DocumentId, Editor, Theme, ViewId, +}; +use once_cell::sync::{Lazy, OnceCell}; +use steel::{ + gc::{unsafe_erased_pointers::CustomReference, ShareableMut}, + rvals::{as_underlying_type, IntoSteelVal, SteelString}, + steel_vm::{ + engine::Engine, mutex_lock, mutex_unlock, register_fn::RegisterFn, ThreadStateController, + }, + steelerr, SteelErr, SteelVal, +}; + +use std::sync::Arc; +use std::{ + borrow::Cow, + collections::HashMap, + error::Error, + path::PathBuf, + sync::{atomic::AtomicBool, Mutex, MutexGuard}, + time::Duration, +}; + +use steel::{rvals::Custom, steel_vm::builtin::BuiltInModule}; + +use crate::{ + commands::insert, + compositor::{self, Component, Compositor}, + config::Config, + events::{OnModeSwitch, PostCommand, PostInsertChar}, + job::{self, Callback}, + keymap::{self, merge_keys, KeyTrie, KeymapResult}, + ui::{self, picker::PathOrId, PickerColumn, Popup, Prompt, PromptEvent}, +}; + +use components::SteelDynamicComponent; + +use super::{ + components::{self, helix_component_module}, + Context, MappableCommand, TYPABLE_COMMAND_LIST, +}; +use insert::{insert_char, insert_string}; + +pub static INTERRUPT_HANDLER: OnceCell = OnceCell::new(); + +// TODO: Use this for the available commands. +// We just have to look at functions that have been defined at +// the top level, _after_ they +pub static GLOBAL_OFFSET: OnceCell = OnceCell::new(); +// pub static AVAILABLE_FUNCTIONS: Lazy>> = Lazy::new(|| RwLock::new(Vec::new())); + +// The Steel scripting engine instance. This is what drives the whole integration. +pub static GLOBAL_ENGINE: Lazy> = Lazy::new(|| { + let engine = steel::steel_vm::engine::Engine::new(); + + // Any function after this point can be used for looking at "new" functions + GLOBAL_OFFSET.set(engine.readable_globals(0).len()).unwrap(); + + let controller = engine.get_thread_state_controller(); + let running = Arc::new(AtomicBool::new(false)); + + fn is_event_available() -> std::io::Result { + crossterm::event::poll(Duration::from_millis(10)) + } + + let controller_clone = controller.clone(); + let running_clone = running.clone(); + + // TODO: Only allow interrupt after a certain amount of time... + // perhaps something like, 500 ms? That way interleaving calls to + // steel functions don't accidentally cause an interrupt. + let thread_handle = std::thread::spawn(move || { + let controller = controller_clone; + let running = running_clone; + + loop { + std::thread::park(); + + while running.load(std::sync::atomic::Ordering::Relaxed) { + if is_event_available().unwrap_or(false) { + let event = crossterm::event::read(); + + if let Ok(Event::Key(crossterm::event::KeyEvent { + code: KeyCode::Char('c'), + modifiers: KeyModifiers::CONTROL, + .. + })) = event + { + controller.interrupt(); + break; + } + } + } + } + }); + + INTERRUPT_HANDLER + .set(InterruptHandler { + controller: controller.clone(), + running: running.clone(), + handle: thread_handle, + }) + .ok(); + + Mutex::new(configure_engine_impl(engine)) +}); + +fn acquire_engine_lock() -> MutexGuard<'static, Engine> { + GLOBAL_ENGINE.lock().unwrap() +} + +/// Run a function with exclusive access to the engine. This only +/// locks the engine that is running on the main thread. +pub fn enter_engine(f: F) -> R +where + F: FnOnce(&mut Engine) -> R, +{ + (f)(&mut acquire_engine_lock()) +} + +pub struct InterruptHandler { + controller: ThreadStateController, + running: Arc, + handle: std::thread::JoinHandle<()>, +} + +pub fn with_interrupt_handler(f: F) -> R +where + F: FnOnce() -> R, +{ + let handler = INTERRUPT_HANDLER.get().unwrap(); + handler.handle.thread().unpark(); + + handler + .running + .store(true, std::sync::atomic::Ordering::Relaxed); + + let res = (f)(); + + handler.controller.resume(); + handler + .running + .store(false, std::sync::atomic::Ordering::Relaxed); + + res +} + +pub struct KeyMapApi { + default_keymap: fn() -> EmbeddedKeyMap, + empty_keymap: fn() -> EmbeddedKeyMap, + string_to_embedded_keymap: fn(String) -> EmbeddedKeyMap, + merge_keybindings: fn(&mut EmbeddedKeyMap, EmbeddedKeyMap), + is_keymap: fn(SteelVal) -> bool, + deep_copy_keymap: fn(EmbeddedKeyMap) -> EmbeddedKeyMap, +} + +impl KeyMapApi { + fn new() -> Self { + KeyMapApi { + default_keymap, + empty_keymap, + string_to_embedded_keymap, + merge_keybindings, + is_keymap, + deep_copy_keymap, + } + } +} + +// Handle buffer and extension specific keybindings in userspace. +pub static BUFFER_OR_EXTENSION_KEYBINDING_MAP: Lazy = + Lazy::new(|| SteelVal::boxed(SteelVal::empty_hashmap())); + +pub static REVERSE_BUFFER_MAP: Lazy = + Lazy::new(|| SteelVal::boxed(SteelVal::empty_hashmap())); + +fn load_component_api(engine: &mut Engine, generate_sources: bool) { + let module = helix_component_module(); + + if generate_sources { + configure_lsp_builtins("component", &module); + } + + engine.register_module(module); +} + +fn load_keymap_api(engine: &mut Engine, api: KeyMapApi, generate_sources: bool) { + let mut module = BuiltInModule::new("helix/core/keymaps"); + + module.register_fn("helix-empty-keymap", api.empty_keymap); + module.register_fn("helix-default-keymap", api.default_keymap); + module.register_fn("helix-merge-keybindings", api.merge_keybindings); + module.register_fn("helix-string->keymap", api.string_to_embedded_keymap); + module.register_fn("keymap?", api.is_keymap); + + module.register_fn("helix-deep-copy-keymap", api.deep_copy_keymap); + + // This should be associated with a corresponding scheme module to wrap this up + module.register_value( + "*buffer-or-extension-keybindings*", + BUFFER_OR_EXTENSION_KEYBINDING_MAP.clone(), + ); + module.register_value("*reverse-buffer-map*", REVERSE_BUFFER_MAP.clone()); + module.register_fn("keymap-update-documentation!", update_documentation); + + if generate_sources { + configure_lsp_builtins("keymap", &module) + } + + engine.register_module(module); +} + +fn load_static_commands(engine: &mut Engine, generate_sources: bool) { + let mut module = BuiltInModule::new("helix/core/static"); + + let mut builtin_static_command_module = if generate_sources { + "(require-builtin helix/core/static as helix.static.)".to_string() + } else { + "".to_string() + }; + + for command in TYPABLE_COMMAND_LIST { + let func = |cx: &mut Context| { + let mut cx = compositor::Context { + editor: cx.editor, + scroll: None, + jobs: cx.jobs, + }; + + (command.fun)(&mut cx, &[], PromptEvent::Validate) + }; + + module.register_fn(command.name, func); + } + + // Register everything in the static command list as well + // These just accept the context, no arguments + for command in MappableCommand::STATIC_COMMAND_LIST { + if let MappableCommand::Static { name, fun, doc } = command { + module.register_fn(name, fun); + + if generate_sources { + let mut docstring = doc + .lines() + .map(|x| { + let mut line = ";;".to_string(); + line.push_str(x); + line.push_str("\n"); + line + }) + .collect::(); + + docstring.pop(); + + builtin_static_command_module.push_str(&format!( + r#" +(provide {}) +;;@doc +{} +(define ({}) + (helix.static.{} *helix.cx*)) +"#, + name, docstring, name, name + )); + } + } + } + + let mut template_function_arity_1 = |name: &str, doc: &str| { + if generate_sources { + let mut docstring = doc + .lines() + .map(|x| { + let mut line = ";;".to_string(); + line.push_str(x); + line.push_str("\n"); + line + }) + .collect::(); + + docstring.pop(); + + builtin_static_command_module.push_str(&format!( + r#" +(provide {}) +;;@doc +{} +(define ({} arg) + (helix.static.{} *helix.cx* arg)) +"#, + name, docstring, name, name + )); + } + }; + + macro_rules! function1 { + ($name:expr, $function:expr, $doc:expr) => {{ + module.register_fn($name, $function); + template_function_arity_1($name, $doc); + }}; + } + + // Adhoc static commands that probably needs evaluating + // Arity 1 + function1!( + "insert_char", + insert_char, + "Insert a given character at the cursor cursor position" + ); + function1!( + "insert_string", + insert_string, + "Insert a given string at the current cursor position" + ); + + function1!( + "set-current-selection-object!", + set_selection, + "Update the selection object to the current selection within the editor" + ); + + function1!( + "regex-selection", + regex_selection, + "Run the given regex within the existing buffer" + ); + + function1!( + "replace-selection-with", + replace_selection, + "Replace the existing selection with the given string" + ); + + function1!( + "cx->current-file", + current_path, + "Get the currently focused file path" + ); + + function1!( + "enqueue-expression-in-engine", + run_expression_in_engine, + "Enqueue an expression to run at the top level context, + after the existing function context has exited." + ); + + let mut template_function_arity_0 = |name: &str| { + if generate_sources { + builtin_static_command_module.push_str(&format!( + r#" +(provide {}) +(define ({}) + (helix.static.{} *helix.cx*)) +"#, + name, name, name + )); + } + }; + + macro_rules! function0 { + ($name:expr, $function:expr) => {{ + module.register_fn($name, $function); + template_function_arity_0($name); + }}; + } + + function0!("current_selection", get_selection); + function0!("load-buffer!", load_buffer); + function0!("current-highlighted-text!", get_highlighted_text); + function0!("get-current-line-number", current_line_number); + function0!("current-selection-object", current_selection); + function0!("get-helix-cwd", get_helix_cwd); + function0!("move-window-far-left", move_window_to_the_left); + function0!("move-window-far-right", move_window_to_the_right); + + let mut template_function_no_context = |name: &str| { + if generate_sources { + builtin_static_command_module.push_str(&format!( + r#" +(provide {}) +(define {} helix.static.{}) + "#, + name, name, name + )) + } + }; + + module.register_fn("get-helix-scm-path", get_helix_scm_path); + module.register_fn("get-init-scm-path", get_init_scm_path); + + template_function_no_context("get-helix-scm-path"); + template_function_no_context("get-init-scm-path"); + + if generate_sources { + let mut target_directory = helix_runtime_search_path(); + + if !target_directory.exists() { + std::fs::create_dir(&target_directory).unwrap(); + } + + target_directory.push("static.scm"); + + std::fs::write(target_directory, builtin_static_command_module).unwrap(); + } + + if generate_sources { + configure_lsp_builtins("static", &module); + } + + engine.register_module(module); +} + +fn load_typed_commands(engine: &mut Engine, generate_sources: bool) { + let mut module = BuiltInModule::new("helix/core/typable".to_string()); + + let mut builtin_typable_command_module = if generate_sources { + "(require-builtin helix/core/typable as helix.)".to_string() + } else { + "".to_string() + }; + + // Register everything in the typable command list. Now these are all available + for command in TYPABLE_COMMAND_LIST { + let func = |cx: &mut Context, args: &[Cow]| { + let mut cx = compositor::Context { + editor: cx.editor, + scroll: None, + jobs: cx.jobs, + }; + + (command.fun)(&mut cx, args, PromptEvent::Validate) + }; + + module.register_fn(command.name, func); + + if generate_sources { + // Create an ephemeral builtin module to reference until I figure out how + // to wrap the functions with a reference to the engine context better. + builtin_typable_command_module.push_str(&format!( + r#" +(provide {}) + +;;@doc +{} +(define ({} . args) + (helix.{} *helix.cx* args)) +"#, + command.name, + { + // Ugly hack to drop the extra newline from + // the docstring + let mut docstring = command + .doc + .lines() + .map(|x| { + let mut line = ";;".to_string(); + line.push_str(x); + line.push_str("\n"); + line + }) + .collect::(); + + docstring.pop(); + + docstring + }, + command.name, + command.name + )); + } + } + + if generate_sources { + let mut target_directory = helix_runtime_search_path(); + if !target_directory.exists() { + std::fs::create_dir(&target_directory).unwrap(); + } + + target_directory.push("commands.scm"); + + std::fs::write(target_directory, builtin_typable_command_module).unwrap(); + } + + if generate_sources { + configure_lsp_builtins("typed", &module); + } + + engine.register_module(module); +} + +// File picker configurations +fn fp_hidden(config: &mut FilePickerConfig, option: bool) { + config.hidden = option; +} + +fn fp_follow_symlinks(config: &mut FilePickerConfig, option: bool) { + config.follow_symlinks = option; +} + +fn fp_deduplicate_links(config: &mut FilePickerConfig, option: bool) { + config.deduplicate_links = option; +} + +fn fp_parents(config: &mut FilePickerConfig, option: bool) { + config.parents = option; +} + +fn fp_ignore(config: &mut FilePickerConfig, option: bool) { + config.ignore = option; +} + +fn fp_git_ignore(config: &mut FilePickerConfig, option: bool) { + config.git_ignore = option; +} + +fn fp_git_global(config: &mut FilePickerConfig, option: bool) { + config.git_global = option; +} + +fn fp_git_exclude(config: &mut FilePickerConfig, option: bool) { + config.git_exclude = option; +} + +fn fp_max_depth(config: &mut FilePickerConfig, option: Option) { + config.max_depth = option; +} + +// Soft wrap configurations +fn sw_enable(config: &mut SoftWrap, option: Option) { + config.enable = option; +} + +fn sw_max_wrap(config: &mut SoftWrap, option: Option) { + config.max_wrap = option; +} + +fn sw_max_indent_retain(config: &mut SoftWrap, option: Option) { + config.max_indent_retain = option; +} + +fn sw_wrap_indicator(config: &mut SoftWrap, option: Option) { + config.wrap_indicator = option; +} + +fn wrap_at_text_width(config: &mut SoftWrap, option: Option) { + config.wrap_at_text_width = option; +} + +fn load_configuration_api(engine: &mut Engine, generate_sources: bool) { + let mut module = BuiltInModule::new("helix/core/configuration"); + + module.register_fn("update-configuration!", |ctx: &mut Context| { + ctx.editor + .config_events + .0 + .send(ConfigEvent::Change) + .unwrap(); + }); + + module + .register_fn("raw-file-picker", || FilePickerConfig::default()) + .register_fn("register-file-picker", HelixConfiguration::file_picker) + .register_fn("fp-hidden", fp_hidden) + .register_fn("fp-follow-symlinks", fp_follow_symlinks) + .register_fn("fp-deduplicate-links", fp_deduplicate_links) + .register_fn("fp-parents", fp_parents) + .register_fn("fp-ignore", fp_ignore) + .register_fn("fp-git-ignore", fp_git_ignore) + .register_fn("fp-git-global", fp_git_global) + .register_fn("fp-git-exclude", fp_git_exclude) + .register_fn("fp-max-depth", fp_max_depth); + + module + .register_fn("raw-soft-wrap", || SoftWrap::default()) + .register_fn("register-soft-wrap", HelixConfiguration::soft_wrap) + .register_fn("sw-enable", sw_enable) + .register_fn("sw-max-wrap", sw_max_wrap) + .register_fn("sw-max-indent-retain", sw_max_indent_retain) + .register_fn("sw-wrap-indicator", sw_wrap_indicator) + .register_fn("sw-wrap-at-text-width", wrap_at_text_width); + + module + .register_fn("scrolloff", HelixConfiguration::scrolloff) + .register_fn("scroll_lines", HelixConfiguration::scroll_lines) + .register_fn("mouse", HelixConfiguration::mouse) + .register_fn("shell", HelixConfiguration::shell) + .register_fn("line-number", HelixConfiguration::line_number) + .register_fn("cursorline", HelixConfiguration::cursorline) + .register_fn("cursorcolumn", HelixConfiguration::cursorcolumn) + .register_fn("middle-click-paste", HelixConfiguration::middle_click_paste) + .register_fn("auto-pairs", HelixConfiguration::auto_pairs) + // Specific constructors for the auto pairs configuration + .register_fn("auto-pairs-default", |enabled: bool| { + AutoPairConfig::Enable(enabled) + }) + .register_fn("auto-pairs-map", |map: HashMap| { + AutoPairConfig::Pairs(map) + }) + // TODO: Finish this up + .register_fn("auto-save-default", || AutoSave::default()) + .register_fn( + "auto-save-after-delay-enable", + HelixConfiguration::auto_save_after_delay_enable, + ) + .register_fn( + "inline-diagnostics-cursor-line-enable", + HelixConfiguration::inline_diagnostics_cursor_line_enable, + ) + .register_fn( + "inline-diagnostics-end-of-line-enable", + HelixConfiguration::inline_diagnostics_end_of_line_enable, + ) + .register_fn("auto-completion", HelixConfiguration::auto_completion) + .register_fn("auto-format", HelixConfiguration::auto_format) + .register_fn("auto-save", HelixConfiguration::auto_save) + .register_fn("text-width", HelixConfiguration::text_width) + .register_fn("idle-timeout", HelixConfiguration::idle_timeout) + .register_fn("completion-timeout", HelixConfiguration::completion_timeout) + .register_fn( + "preview-completion-insert", + HelixConfiguration::preview_completion_insert, + ) + .register_fn( + "completion-trigger-len", + HelixConfiguration::completion_trigger_len, + ) + .register_fn("completion-replace", HelixConfiguration::completion_replace) + .register_fn("auto-info", HelixConfiguration::auto_info) + .register_fn("cursor-shape", HelixConfiguration::cursor_shape) + .register_fn("true-color", HelixConfiguration::true_color) + .register_fn( + "insert-final-newline", + HelixConfiguration::insert_final_newline, + ) + .register_fn("color-modes", HelixConfiguration::color_modes) + .register_fn("gutters", HelixConfiguration::gutters) + // .register_fn("file-picker", HelixConfiguration::file_picker) + .register_fn("statusline", HelixConfiguration::statusline) + .register_fn("undercurl", HelixConfiguration::undercurl) + .register_fn("search", HelixConfiguration::search) + .register_fn("lsp", HelixConfiguration::lsp) + .register_fn("terminal", HelixConfiguration::terminal) + .register_fn("rulers", HelixConfiguration::rulers) + .register_fn("whitespace", HelixConfiguration::whitespace) + .register_fn("bufferline", HelixConfiguration::bufferline) + .register_fn("indent-guides", HelixConfiguration::indent_guides) + .register_fn("soft-wrap", HelixConfiguration::soft_wrap) + .register_fn( + "workspace-lsp-roots", + HelixConfiguration::workspace_lsp_roots, + ) + .register_fn( + "default-line-ending", + HelixConfiguration::default_line_ending, + ) + .register_fn("smart-tab", HelixConfiguration::smart_tab); + + // Keybinding stuff + module + .register_fn("keybindings", HelixConfiguration::keybindings) + .register_fn("get-keybindings", HelixConfiguration::get_keybindings); + + if generate_sources { + let mut builtin_configuration_module = + "(require-builtin helix/core/configuration as helix.)".to_string(); + + builtin_configuration_module.push_str(&format!( + r#" +(provide update-configuration!) +(define (update-configuration!) + (helix.update-configuration! *helix.config*)) +"#, + )); + + // Register the get keybindings function + builtin_configuration_module.push_str(&format!( + r#" +(provide get-keybindings) +(define (get-keybindings) + (helix.get-keybindings *helix.config*)) +"#, + )); + + let mut template_soft_wrap = |name: &str| { + builtin_configuration_module.push_str(&format!( + r#" +(provide {}) +(define ({} arg) + (lambda (picker) + (helix.{} picker arg) + picker)) +"#, + name, name, name + )); + }; + + let soft_wrap_functions = &[ + "sw-enable", + "sw-max-wrap", + "sw-max-indent-retain", + "sw-wrap-indicator", + "sw-wrap-at-text-width", + ]; + + for name in soft_wrap_functions { + template_soft_wrap(name); + } + + let mut template_file_picker_function = |name: &str| { + builtin_configuration_module.push_str(&format!( + r#" +(provide {}) +(define ({} arg) + (lambda (picker) + (helix.{} picker arg) + picker)) +"#, + name, name, name + )); + }; + + let file_picker_functions = &[ + "fp-hidden", + "fp-follow-symlinks", + "fp-deduplicate-links", + "fp-parents", + "fp-ignore", + "fp-git-ignore", + "fp-git-global", + "fp-git-exclude", + "fp-max-depth", + ]; + + for name in file_picker_functions { + template_file_picker_function(name); + } + + builtin_configuration_module.push_str(&format!( + r#" +(provide file-picker) +(define (file-picker . args) + (helix.register-file-picker + *helix.config* + (foldl (lambda (func config) (func config)) (helix.raw-file-picker) args))) +"#, + )); + + builtin_configuration_module.push_str(&format!( + r#" +(provide soft-wrap) +(define (soft-wrap . args) + (helix.register-soft-wrap + *helix.config* + (foldl (lambda (func config) (func config)) (helix.raw-soft-wrap) args))) +"#, + )); + + let mut template_function_arity_1 = |name: &str| { + builtin_configuration_module.push_str(&format!( + r#" +(provide {}) +(define ({} arg) + (helix.{} *helix.config* arg)) +"#, + name, name, name + )); + }; + + let functions = &[ + "scrolloff", + "scroll_lines", + "mouse", + "shell", + "line-number", + "cursorline", + "cursorcolumn", + "middle-click-paste", + "auto-pairs", + "auto-completion", + "auto-format", + "auto-save", + "text-width", + "idle-timeout", + "completion-timeout", + "preview-completion-insert", + "completion-trigger-len", + "completion-replace", + "auto-info", + "cursor-shape", + "true-color", + "insert-final-newline", + "color-modes", + "gutters", + "statusline", + "undercurl", + "search", + "lsp", + "terminal", + "rulers", + "whitespace", + "bufferline", + "indent-guides", + "workspace-lsp-roots", + "default-line-ending", + "smart-tab", + "keybindings", + "inline-diagnostics-cursor-line-enable", + "inline-diagnostics-end-of-line-enable", + ]; + + for func in functions { + template_function_arity_1(func); + } + + let mut target_directory = helix_runtime_search_path(); + + if !target_directory.exists() { + std::fs::create_dir(&target_directory).unwrap(); + } + + target_directory.push("configuration.scm"); + + std::fs::write(target_directory, builtin_configuration_module).unwrap(); + } + + if generate_sources { + configure_lsp_builtins("configuration", &module); + } + + engine.register_module(module); +} + +fn languages_api(engine: &mut Engine, generate_sources: bool) { + // TODO: Just look at the `cx.editor.syn_loader` for how to + // manipulate the languages bindings + todo!() +} + +// fn test(ctx: &mut Context) { +// ctx.editor.syn_loader.load() +// } + +// TODO: +// This isn't the best API since it pretty much requires deserializing +// the whole theme model each time. While its not _horrible_, it is +// certainly not as efficient as it could be. If we could just edit +// the loaded theme in memory already, then it would be a bit nicer. +fn load_theme_api(engine: &mut Engine, generate_sources: bool) { + let mut module = BuiltInModule::new("helix/core/themes"); + module + .register_fn("hashmap->theme", theme_from_json_string) + .register_fn("add-theme!", add_theme) + .register_fn("theme-style", get_style) + .register_fn("theme-set-style!", set_style) + .register_fn("string->color", string_to_color); + + if generate_sources { + configure_lsp_builtins("themes", &module); + } + + engine.register_module(module); +} + +#[derive(Clone)] +struct SteelTheme(Theme); +impl Custom for SteelTheme {} + +fn theme_from_json_string(name: String, value: SteelVal) -> Result { + // TODO: Really don't love this at all. The deserialization should be a bit more elegant + let json_value = serde_json::Value::try_from(value)?; + let value: toml::Value = serde_json::from_str(&serde_json::to_string(&json_value)?)?; + + let (mut theme, _) = Theme::from_toml(value); + theme.set_name(name); + Ok(SteelTheme(theme)) +} + +// Mutate the theme? +fn add_theme(cx: &mut Context, theme: SteelTheme) { + cx.editor + .user_defined_themes + .insert(theme.0.name().to_owned(), theme.0); +} + +fn get_style(theme: &SteelTheme, name: SteelString) -> helix_view::theme::Style { + theme.0.get(name.as_str()).clone() +} + +fn set_style(theme: &mut SteelTheme, name: String, style: helix_view::theme::Style) { + theme.0.set(name, style) +} + +fn string_to_color(string: SteelString) -> Result { + // TODO: Don't expose this directly + helix_view::theme::ThemePalette::string_to_rgb(string.as_str()).map_err(anyhow::Error::msg) +} + +fn current_buffer_area(cx: &mut Context) -> Option { + let focus = cx.editor.tree.focus; + cx.editor.tree.view_id_area(focus) +} + +fn load_editor_api(engine: &mut Engine, generate_sources: bool) { + let mut module = BuiltInModule::new("helix/core/editor"); + + // Types + module.register_fn("Action/Load", || Action::Load); + module.register_fn("Action/Replace", || Action::Replace); + module.register_fn("Action/HorizontalSplit", || Action::HorizontalSplit); + module.register_fn("Action/VerticalSplit", || Action::VerticalSplit); + + // Arity 0 + module.register_fn("editor-focus", cx_current_focus); + module.register_fn("editor-mode", cx_get_mode); + module.register_fn("cx->themes", get_themes); + module.register_fn("editor-all-documents", cx_editor_all_documents); + module.register_fn("cx->cursor", |cx: &mut Context| cx.editor.cursor()); + + // Arity 1 + module.register_fn("editor->doc-id", cx_get_document_id); + module.register_fn("editor-switch!", cx_switch); + module.register_fn("editor-set-focus!", |cx: &mut Context, view_id: ViewId| { + cx.editor.focus(view_id) + }); + module.register_fn("editor-set-mode!", cx_set_mode); + module.register_fn("editor-doc-in-view?", cx_is_document_in_view); + module.register_fn("set-scratch-buffer-name!", set_scratch_buffer_name); + module.register_fn("editor-doc-exists?", cx_document_exists); + + // Arity 2 + module.register_fn("editor-switch-action!", cx_switch_action); + + // Arity 1 + module.register_fn("editor->text", document_id_to_text); + module.register_fn("editor-document->path", document_path); + + module.register_fn("set-editor-clip-right!", |cx: &mut Context, right: u16| { + cx.editor.editor_clipping.right = Some(right); + }); + module.register_fn("set-editor-clip-left!", |cx: &mut Context, left: u16| { + cx.editor.editor_clipping.left = Some(left); + }); + module.register_fn("set-editor-clip-top!", |cx: &mut Context, top: u16| { + cx.editor.editor_clipping.top = Some(top); + }); + module.register_fn( + "set-editor-clip-bottom!", + |cx: &mut Context, bottom: u16| { + cx.editor.editor_clipping.bottom = Some(bottom); + }, + ); + + module.register_fn("editor-focused-buffer-area", current_buffer_area); + + if generate_sources { + let mut builtin_editor_command_module = + "(require-builtin helix/core/editor as helix.)".to_string(); + + let mut template_function_type_constructor = |name: &str| { + builtin_editor_command_module.push_str(&format!( + r#" +(provide {}) +(define ({}) + (helix.{})) +"#, + name, name, name + )); + }; + + template_function_type_constructor("Action/Load"); + template_function_type_constructor("Action/Replace"); + template_function_type_constructor("Action/HorizontalSplit"); + template_function_type_constructor("Action/VerticalSplit"); + + let mut template_function_arity_0 = |name: &str| { + builtin_editor_command_module.push_str(&format!( + r#" +(provide {}) +(define ({}) + (helix.{} *helix.cx*)) +"#, + name, name, name + )); + }; + + template_function_arity_0("editor-focus"); + template_function_arity_0("editor-mode"); + template_function_arity_0("cx->themes"); + template_function_arity_0("editor-all-documents"); + template_function_arity_0("cx->cursor"); + template_function_arity_0("editor-focused-buffer-area"); + + let mut template_function_arity_1 = |name: &str| { + builtin_editor_command_module.push_str(&format!( + r#" +(provide {}) +(define ({} arg) + (helix.{} *helix.cx* arg)) +"#, + name, name, name + )); + }; + + template_function_arity_1("editor->doc-id"); + template_function_arity_1("editor-switch!"); + template_function_arity_1("editor-set-focus!"); + template_function_arity_1("editor-set-mode!"); + template_function_arity_1("editor-doc-in-view?"); + template_function_arity_1("set-scratch-buffer-name!"); + template_function_arity_1("editor-doc-exists?"); + template_function_arity_1("editor->text"); + template_function_arity_1("editor-document->path"); + + template_function_arity_1("set-editor-clip-top!"); + template_function_arity_1("set-editor-clip-right!"); + template_function_arity_1("set-editor-clip-left!"); + template_function_arity_1("set-editor-clip-bottom!"); + + let mut template_function_arity_2 = |name: &str| { + builtin_editor_command_module.push_str(&format!( + r#" +(provide {}) +(define ({} arg1 arg2) + (helix.{} *helix.cx* arg1 arg2)) +"#, + name, name, name + )); + }; + + template_function_arity_2("editor-switch-action!"); + + let mut target_directory = helix_runtime_search_path(); + + if !target_directory.exists() { + std::fs::create_dir_all(&target_directory).unwrap_or_else(|err| { + panic!("Failed to create directory {:?}: {}", target_directory, err) + }); + eprintln!("Created directory: {:?}", target_directory); + } + + target_directory.push("editor.scm"); + + std::fs::write(target_directory, builtin_editor_command_module).unwrap(); + } + + // Generate the lsp configuration + if generate_sources { + configure_lsp_builtins("editor", &module); + } + + engine.register_module(module); +} + +pub struct SteelScriptingEngine; + +impl super::PluginSystem for SteelScriptingEngine { + fn initialize(&self) { + initialize_engine(); + } + + fn engine_name(&self) -> super::PluginSystemKind { + super::PluginSystemKind::Steel + } + + fn run_initialization_script( + &self, + cx: &mut Context, + configuration: Arc>>, + ) { + run_initialization_script(cx, configuration); + } + + fn handle_keymap_event( + &self, + editor: &mut ui::EditorView, + mode: Mode, + cxt: &mut Context, + event: KeyEvent, + ) -> Option { + SteelScriptingEngine::get_keymap_for_extension(cxt).and_then(|map| { + if let steel::SteelVal::Custom(inner) = map { + if let Some(underlying) = + steel::rvals::as_underlying_type::(inner.read().as_ref()) + { + return Some(editor.keymaps.get_with_map(&underlying.0, mode, event)); + } + } + + None + }) + } + + fn call_function_by_name(&self, cx: &mut Context, name: &str, args: &[Cow]) -> bool { + if enter_engine(|x| x.global_exists(name)) { + let args = args + .iter() + .map(|x| x.clone().into_steelval().unwrap()) + .collect::>(); + + if let Err(e) = enter_engine(|guard| { + { + // Install the interrupt handler, in the event this thing + // is blocking for too long. + with_interrupt_handler(|| { + guard.with_mut_reference::(cx).consume( + move |engine, arguments| { + let context = arguments[0].clone(); + engine.update_value("*helix.cx*", context); + + // TODO: Get rid of this clone + engine.call_function_by_name_with_args(name, args.clone()) + }, + ) + }) + } + }) { + cx.editor.set_error(format!("{}", e)); + } + true + } else { + false + } + } + + fn call_typed_command<'a>( + &self, + cx: &mut compositor::Context, + input: &'a str, + parts: &'a [&'a str], + event: PromptEvent, + ) -> bool { + if enter_engine(|x| x.global_exists(parts[0])) { + let shellwords = Shellwords::from(input); + let args = shellwords.words(); + + // 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[1..] + .iter() + .map(|x| x.clone().into_steelval().unwrap()) + .collect::>(); + + let res = { + let mut ctx = Context { + register: None, + count: std::num::NonZeroUsize::new(1), + editor: cx.editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs: cx.jobs, + }; + + // Install interrupt handler here during the duration + // of the function call + match with_interrupt_handler(|| { + guard + .with_mut_reference(&mut ctx) + .consume(move |engine, arguments| { + let context = arguments[0].clone(); + engine.update_value("*helix.cx*", context); + // TODO: Fix this clone + engine.call_function_by_name_with_args(&parts[0], args.clone()) + }) + }) { + Ok(res) => { + cx.editor.set_status(res.to_string()); + Ok(res) + } + Err(e) => Err(e), + } + }; + + res + }) { + let mut ctx = Context { + register: None, + count: None, + editor: &mut cx.editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs: &mut cx.jobs, + }; + + enter_engine(|x| present_error_inside_engine_context(&mut ctx, x, e)); + }; + } + + // Global exists + true + } else { + // Global does not exist + false + } + } + + fn get_doc_for_identifier(&self, ident: &str) -> Option { + enter_engine(|engine| get_doc_for_global(engine, ident)) + } + + // Just dump docs for all top level values? + fn available_commands<'a>(&self) -> Vec> { + enter_engine(|engine| { + engine + .readable_globals(*GLOBAL_OFFSET.get().unwrap()) + .iter() + .map(|x| x.resolve().to_string().into()) + .collect() + }) + } + + fn generate_sources(&self) { + // Generate sources directly with a fresh engine + let mut engine = Engine::new(); + configure_builtin_sources(&mut engine, true); + // Generate documentation as well + let target = helix_runtime_search_path(); + + let mut writer = std::io::BufWriter::new(std::fs::File::create("steel-docs.md").unwrap()); + + // Generate markdown docs + steel_doc::walk_dir(&mut writer, target, &mut engine).unwrap(); + } +} + +impl SteelScriptingEngine { + // Attempt to fetch the keymap for the extension + fn get_keymap_for_extension<'a>(cx: &'a mut Context) -> Option { + // Get the currently activated extension, also need to check the + // buffer type. + let extension = { + let current_focus = cx.editor.tree.focus; + let view = cx.editor.tree.get(current_focus); + let doc = &view.doc; + let current_doc = cx.editor.documents.get(doc); + + current_doc + .and_then(|x| x.path()) + .and_then(|x| x.extension()) + .and_then(|x| x.to_str()) + }; + + let doc_id = { + let current_focus = cx.editor.tree.focus; + let view = cx.editor.tree.get(current_focus); + let doc = &view.doc; + + doc + }; + + if let Some(extension) = extension { + if let SteelVal::Boxed(boxed_map) = BUFFER_OR_EXTENSION_KEYBINDING_MAP.clone() { + if let SteelVal::HashMapV(map) = boxed_map.read().clone() { + if let Some(value) = map.get(&SteelVal::StringV(extension.into())) { + if let SteelVal::Custom(inner) = value { + if let Some(_) = steel::rvals::as_underlying_type::( + inner.read().as_ref(), + ) { + return Some(value.clone()); + } + } + } + } + } + } + + if let SteelVal::Boxed(boxed_map) = REVERSE_BUFFER_MAP.clone() { + if let SteelVal::HashMapV(map) = boxed_map.read().clone() { + if let Some(label) = map.get(&SteelVal::IntV(document_id_to_usize(doc_id) as isize)) + { + if let SteelVal::Boxed(boxed_map) = BUFFER_OR_EXTENSION_KEYBINDING_MAP.clone() { + if let SteelVal::HashMapV(map) = boxed_map.read().clone() { + if let Some(value) = map.get(label) { + if let SteelVal::Custom(inner) = value { + if let Some(_) = + steel::rvals::as_underlying_type::( + inner.read().as_ref(), + ) + { + return Some(value.clone()); + } + } + } + } + } + } + } + } + + None + } +} + +pub fn initialize_engine() { + enter_engine(|x| x.globals().first().copied()); +} + +pub fn present_error_inside_engine_context(cx: &mut Context, engine: &mut Engine, e: SteelErr) { + cx.editor.set_error(e.to_string()); + + let backtrace = engine.raise_error_to_string(e); + + let callback = async move { + let call: job::Callback = Callback::EditorCompositor(Box::new( + move |editor: &mut Editor, compositor: &mut Compositor| { + if let Some(backtrace) = backtrace { + let contents = ui::Markdown::new( + format!("```\n{}\n```", backtrace), + editor.syn_loader.clone(), + ); + let popup = Popup::new("engine", contents).position(Some( + helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2), + )); + compositor.replace_or_push("engine", popup); + } + }, + )); + Ok(call) + }; + cx.jobs.callback(callback); +} + +// Key maps +#[derive(Clone, Debug)] +pub struct EmbeddedKeyMap(pub HashMap); +impl Custom for EmbeddedKeyMap {} + +pub fn update_documentation(map: &mut EmbeddedKeyMap, docs: HashMap) { + let mut func = move |command: &mut MappableCommand| { + if let Some(steel_doc) = docs.get(command.name()) { + if let Some(doc) = command.doc_mut() { + *doc = steel_doc.to_owned() + } + } + }; + + for trie in map.0.values_mut() { + trie.apply(&mut func) + } +} + +// Will deep copy a value by default when using a value type +pub fn deep_copy_keymap(copied: EmbeddedKeyMap) -> EmbeddedKeyMap { + copied +} + +// Base level - no configuration +pub fn default_keymap() -> EmbeddedKeyMap { + EmbeddedKeyMap(keymap::default()) +} + +// Completely empty, allow for overriding +pub fn empty_keymap() -> EmbeddedKeyMap { + EmbeddedKeyMap(HashMap::default()) +} + +pub fn string_to_embedded_keymap(value: String) -> EmbeddedKeyMap { + EmbeddedKeyMap(serde_json::from_str(&value).unwrap()) +} + +pub fn merge_keybindings(left: &mut EmbeddedKeyMap, right: EmbeddedKeyMap) { + merge_keys(&mut left.0, right.0) +} + +pub fn is_keymap(keymap: SteelVal) -> bool { + if let SteelVal::Custom(underlying) = keymap { + as_underlying_type::(underlying.read().as_ref()).is_some() + } else { + false + } +} + +fn local_config_exists() -> bool { + let local_helix = find_workspace().0.join(".helix"); + local_helix.join("helix.scm").exists() && local_helix.join("init.scm").exists() +} + +fn preferred_config_path(file_name: &str) -> PathBuf { + if local_config_exists() { + find_workspace().0.join(".helix").join(file_name) + } else { + helix_loader::config_dir().join(file_name) + } +} + +pub fn helix_module_file() -> PathBuf { + preferred_config_path("helix.scm") +} + +pub fn steel_init_file() -> PathBuf { + preferred_config_path("init.scm") +} + +#[derive(Clone)] +struct HelixConfiguration { + configuration: Arc>>, +} + +impl Custom for HelixConfiguration {} +// impl Custom for LineNumber {} + +impl HelixConfiguration { + fn load_config(&self) -> Config { + (*self.configuration.load().clone()).clone() + } + + fn store_config(&self, config: Config) { + self.configuration.store(Arc::new(config)); + } + + // Overlay new keybindings + fn keybindings(&self, keybindings: EmbeddedKeyMap) { + let mut app_config = self.load_config(); + merge_keys(&mut app_config.keys, keybindings.0); + self.store_config(app_config); + } + + fn get_keybindings(&self) -> EmbeddedKeyMap { + EmbeddedKeyMap(self.load_config().keys.clone()) + } + + fn scrolloff(&self, lines: usize) { + let mut app_config = self.load_config(); + app_config.editor.scrolloff = lines; + self.store_config(app_config); + } + + fn scroll_lines(&self, lines: isize) { + let mut app_config = self.load_config(); + app_config.editor.scroll_lines = lines; + self.store_config(app_config); + } + + fn mouse(&self, m: bool) { + let mut app_config = self.load_config(); + app_config.editor.mouse = m; + self.store_config(app_config); + } + + fn shell(&self, shell: Vec) { + let mut app_config = self.load_config(); + app_config.editor.shell = shell; + self.store_config(app_config); + } + + // TODO: Make this a symbol, probably! + fn line_number(&self, mode: LineNumber) { + let mut app_config = self.load_config(); + app_config.editor.line_number = mode; + self.store_config(app_config); + } + + fn cursorline(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.cursorline = option; + self.store_config(app_config); + } + + fn cursorcolumn(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.cursorcolumn = option; + self.store_config(app_config); + } + + fn middle_click_paste(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.middle_click_paste = option; + self.store_config(app_config); + } + + fn auto_pairs(&self, config: AutoPairConfig) { + let mut app_config = self.load_config(); + app_config.editor.auto_pairs = config; + self.store_config(app_config); + } + + fn auto_completion(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.auto_completion = option; + self.store_config(app_config); + } + + fn auto_format(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.auto_format = option; + self.store_config(app_config); + } + + fn auto_save(&self, option: AutoSave) { + let mut app_config = self.load_config(); + app_config.editor.auto_save = option; + self.store_config(app_config); + } + + // TODO: Finish the auto save options! + fn auto_save_after_delay_enable(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.auto_save.after_delay.enable = option; + self.store_config(app_config); + } + + // TODO: Finish diagnostic options! + fn inline_diagnostics_cursor_line_enable(&self, severity: String) { + let mut app_config = self.load_config(); + let severity = match severity.as_str() { + "hint" => Severity::Hint, + "info" => Severity::Info, + "warning" => Severity::Warning, + "error" => Severity::Error, + _ => return, + }; + app_config.editor.inline_diagnostics.cursor_line = DiagnosticFilter::Enable(severity); + self.store_config(app_config); + } + + fn inline_diagnostics_end_of_line_enable(&self, severity: String) { + let mut app_config = self.load_config(); + let severity = match severity.as_str() { + "hint" => Severity::Hint, + "info" => Severity::Info, + "warning" => Severity::Warning, + "error" => Severity::Error, + _ => return, + }; + app_config.editor.end_of_line_diagnostics = DiagnosticFilter::Enable(severity); + self.store_config(app_config); + } + + fn text_width(&self, width: usize) { + let mut app_config = self.load_config(); + app_config.editor.text_width = width; + self.store_config(app_config); + } + + fn idle_timeout(&self, ms: usize) { + let mut app_config = self.load_config(); + app_config.editor.idle_timeout = Duration::from_millis(ms as u64); + self.store_config(app_config); + } + + fn completion_timeout(&self, ms: usize) { + let mut app_config = self.load_config(); + app_config.editor.completion_timeout = Duration::from_millis(ms as u64); + self.store_config(app_config); + } + + fn preview_completion_insert(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.preview_completion_insert = option; + self.store_config(app_config); + } + + // TODO: Make sure this conversion works automatically + fn completion_trigger_len(&self, length: u8) { + let mut app_config = self.load_config(); + app_config.editor.completion_trigger_len = length; + self.store_config(app_config); + } + + fn completion_replace(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.completion_replace = option; + self.store_config(app_config); + } + + fn auto_info(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.auto_info = option; + self.store_config(app_config); + } + + fn cursor_shape(&self, config: CursorShapeConfig) { + let mut app_config = self.load_config(); + app_config.editor.cursor_shape = config; + self.store_config(app_config); + } + + fn true_color(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.true_color = option; + self.store_config(app_config); + } + + fn insert_final_newline(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.insert_final_newline = option; + self.store_config(app_config); + } + + fn color_modes(&self, option: bool) { + let mut app_config = self.load_config(); + app_config.editor.color_modes = option; + self.store_config(app_config); + } + + fn gutters(&self, config: GutterConfig) { + let mut app_config = self.load_config(); + app_config.editor.gutters = config; + self.store_config(app_config); + } + + fn file_picker(&self, picker: FilePickerConfig) { + let mut app_config = self.load_config(); + app_config.editor.file_picker = picker; + self.store_config(app_config); + } + + fn statusline(&self, config: StatusLineConfig) { + let mut app_config = self.load_config(); + app_config.editor.statusline = config; + self.store_config(app_config); + } + + fn undercurl(&self, undercurl: bool) { + let mut app_config = self.load_config(); + app_config.editor.undercurl = undercurl; + self.store_config(app_config); + } + + fn search(&self, config: SearchConfig) { + let mut app_config = self.load_config(); + app_config.editor.search = config; + self.store_config(app_config); + } + + fn lsp(&self, config: LspConfig) { + let mut app_config = self.load_config(); + app_config.editor.lsp = config; + self.store_config(app_config); + } + + fn terminal(&self, config: Option) { + let mut app_config = self.load_config(); + app_config.editor.terminal = config; + self.store_config(app_config); + } + + fn rulers(&self, cols: Vec) { + let mut app_config = self.load_config(); + app_config.editor.rulers = cols; + self.store_config(app_config); + } + + fn whitespace(&self, config: WhitespaceConfig) { + let mut app_config = self.load_config(); + app_config.editor.whitespace = config; + self.store_config(app_config); + } + + fn bufferline(&self, config: BufferLine) { + let mut app_config = self.load_config(); + app_config.editor.bufferline = config; + self.store_config(app_config); + } + + fn indent_guides(&self, config: IndentGuidesConfig) { + let mut app_config = self.load_config(); + app_config.editor.indent_guides = config; + self.store_config(app_config); + } + + fn soft_wrap(&self, config: SoftWrap) { + let mut app_config = self.load_config(); + app_config.editor.soft_wrap = config; + self.store_config(app_config); + } + + fn workspace_lsp_roots(&self, roots: Vec) { + let mut app_config = self.load_config(); + app_config.editor.workspace_lsp_roots = roots; + self.store_config(app_config); + } + + fn default_line_ending(&self, config: LineEndingConfig) { + let mut app_config = self.load_config(); + app_config.editor.default_line_ending = config; + self.store_config(app_config); + } + + fn smart_tab(&self, config: Option) { + let mut app_config = self.load_config(); + app_config.editor.smart_tab = config; + self.store_config(app_config); + } +} + +// Get doc from function ptr table, hack +fn get_doc_for_global(engine: &mut Engine, ident: &str) -> Option { + if engine.global_exists(ident) { + let expr = format!("(#%function-ptr-table-get #%function-ptr-table {})", ident); + Some( + engine + .run(expr) + .ok() + .and_then(|x| x.first().cloned()) + .and_then(|x| x.as_string().map(|x| x.as_str().to_string())) + .unwrap_or_else(|| "Undocumented plugin command".to_string()), + ) + } else { + None + } +} + +/// Run the initialization script located at `$helix_config/init.scm` +/// This runs the script in the global environment, and does _not_ load it as a module directly +fn run_initialization_script(cx: &mut Context, configuration: Arc>>) { + log::info!("Loading init.scm..."); + + let helix_module_path = helix_module_file(); + + // TODO: Report the error from requiring the file! + enter_engine(|guard| { + // Embed the configuration so we don't have to communicate over the refresh + // channel. The state is still stored within the `Application` struct, but + // now we can just access it and signal a refresh of the config when we need to. + guard.update_value( + "*helix.config*", + HelixConfiguration { configuration } + .into_steelval() + .unwrap(), + ); + + let res = guard.run_with_reference( + cx, + "*helix.cx*", + &format!(r#"(require {:?})"#, helix_module_path.to_str().unwrap()), + ); + + // Present the error in the helix.scm loading + if let Err(e) = res { + present_error_inside_engine_context(cx, guard, e); + return; + } + + let helix_module_path = steel_init_file(); + + // These contents need to be registered with the path? + if let Ok(contents) = std::fs::read_to_string(&helix_module_path) { + let res = guard.run_with_reference_from_path::( + cx, + "*helix.cx*", + &contents, + helix_module_path, + ); + + match res { + Ok(_) => {} + Err(e) => present_error_inside_engine_context(cx, guard, e), + } + + log::info!("Finished loading init.scm!") + } else { + log::info!("No init.scm found, skipping loading.") + } + }); +} + +impl Custom for PromptEvent {} + +impl<'a> CustomReference for Context<'a> {} + +steel::custom_reference!(Context<'a>); + +fn get_themes(cx: &mut Context) -> Vec { + ui::completers::theme(cx.editor, "") + .into_iter() + .map(|x| x.1.to_string()) + .collect() +} + +/// A dynamic component, used for rendering thing +impl Custom for compositor::EventResult {} + +pub struct WrappedDynComponent { + pub(crate) inner: Option>, +} + +impl Custom for WrappedDynComponent {} + +pub struct BoxDynComponent { + inner: Box, +} + +impl BoxDynComponent { + pub fn new(inner: Box) -> Self { + Self { inner } + } +} + +impl Component for BoxDynComponent { + fn handle_event( + &mut self, + _event: &helix_view::input::Event, + _ctx: &mut compositor::Context, + ) -> compositor::EventResult { + self.inner.handle_event(_event, _ctx) + } + + fn should_update(&self) -> bool { + self.inner.should_update() + } + + fn cursor( + &self, + _area: helix_view::graphics::Rect, + _ctx: &Editor, + ) -> ( + Option, + helix_view::graphics::CursorKind, + ) { + self.inner.cursor(_area, _ctx) + } + + fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> { + self.inner.required_size(_viewport) + } + + fn type_name(&self) -> &'static str { + std::any::type_name::() + } + + fn id(&self) -> Option<&'static str> { + Some(self.inner.type_name()) + } + + fn name(&self) -> Option<&str> { + self.inner.name() + } + + fn render( + &mut self, + area: helix_view::graphics::Rect, + frame: &mut tui::buffer::Buffer, + ctx: &mut compositor::Context, + ) { + self.inner.render(area, frame, ctx) + } +} + +#[derive(Debug, Clone, Copy)] +struct OnModeSwitchEvent { + old_mode: Mode, + new_mode: Mode, +} + +impl OnModeSwitchEvent { + pub fn get_old_mode(&self) -> Mode { + self.old_mode + } + + pub fn get_new_mode(&self) -> Mode { + self.new_mode + } +} + +impl Custom for OnModeSwitchEvent {} +impl Custom for MappableCommand {} + +// Don't take the function name, just take the function itself? +fn register_hook(event_kind: String, callback_fn: SteelVal) -> steel::UnRecoverableResult { + let rooted = callback_fn.as_rooted(); + + match event_kind.as_str() { + "on-mode-switch" => { + register_hook!(move |event: &mut OnModeSwitch<'_, '_>| { + // if enter_engine(|x| x.global_exists(&function_name)) { + if let Err(e) = enter_engine(|guard| { + let minimized_event = OnModeSwitchEvent { + old_mode: event.old_mode, + new_mode: event.new_mode, + }; + + guard.with_mut_reference(event.cx).consume(|engine, args| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + + let mut args = vec![minimized_event.into_steelval().unwrap()]; + // engine.call_function_by_name_with_args(&function_name, args) + engine.call_function_with_args_from_mut_slice( + rooted.value().clone(), + &mut args, + ) + }) + }) { + event.cx.editor.set_error(e.to_string()); + } + // } + + Ok(()) + }); + + Ok(SteelVal::Void).into() + } + "post-insert-char" => { + register_hook!(move |event: &mut PostInsertChar<'_, '_>| { + // if enter_engine(|x| x.global_exists(&function_name)) { + if let Err(e) = enter_engine(|guard| { + guard.with_mut_reference(event.cx).consume(|engine, args| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + + // args.push(event.c.into()); + // engine.call_function_by_name_with_args(&function_name, vec![event.c.into()]) + + let mut args = vec![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 enter_engine(|x| x.global_exists(&function_name)) { + if let Err(e) = enter_engine(|guard| { + guard.with_mut_reference(event.cx).consume(|engine, args| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + + let mut args = vec![event.command.name().into_steelval().unwrap()]; + + engine.call_function_with_args_from_mut_slice( + rooted.value().clone(), + &mut args, + ) + + // args.push(event.command.clone().into_steelval().unwrap()); + // engine.call_function_by_name_with_args( + // &function_name, + // // Name? + // vec![event.command.name().into_steelval().unwrap()], + // ) + }) + }) { + event.cx.editor.set_error(e.to_string()); + } + // } + + Ok(()) + }); + + Ok(SteelVal::Void).into() + } + // Unimplemented! + // "document-did-change" => { + // todo!() + // } + // "selection-did-change" => { + // todo!() + // } + _ => steelerr!(Generic => "Unable to register hook: Unknown event type: {}", event_kind) + .into(), + } +} + +fn configure_lsp_globals() { + if let Ok(steel_lsp_home) = std::env::var("STEEL_LSP_HOME") { + let mut path = PathBuf::from(steel_lsp_home); + path.push("_helix-global-builtins.scm"); + + let mut output = String::new(); + + let names = &[ + "*helix.cx*", + "*helix.config*", + "*helix.id*", + "register-hook!", + "log::info!", + "fuzzy-match", + "helix-find-workspace", + "doc-id->usize", + "new-component!", + "acquire-context-lock", + "SteelDynamicComponent?", + "prompt", + "picker", + "Component::Text", + "hx.create-directory", + ]; + + for value in names { + use std::fmt::Write; + writeln!(&mut output, "(#%register-global '{})", value).unwrap(); + } + + std::fs::write(path, output).unwrap(); + } +} + +fn configure_lsp_builtins(name: &str, module: &BuiltInModule) { + if let Ok(steel_lsp_home) = std::env::var("STEEL_LSP_HOME") { + let mut path = PathBuf::from(steel_lsp_home); + path.push(&format!("_helix-{}-builtins.scm", name)); + + let mut output = String::new(); + + output.push_str(&format!( + r#"(define #%helix-{}-module (#%module "{}")) + +(define (register-values module values) + (map (lambda (ident) (#%module-add module (symbol->string ident) void)) values)) +"#, + name, + module.name() + )); + + output.push_str(&format!(r#"(register-values #%helix-{}-module '("#, name)); + + for value in module.names() { + use std::fmt::Write; + writeln!(&mut output, "{}", value).unwrap(); + } + + output.push_str("))"); + + std::fs::write(path, output).unwrap(); + } +} + +fn load_rope_api(engine: &mut Engine, generate_sources: bool) { + // Wrap the rope module? + let rope_slice_module = rope_module(); + + if generate_sources { + configure_lsp_builtins("rope", &rope_slice_module); + } + + engine.register_module(rope_slice_module); +} + +// struct SteelEngine(Engine); + +// impl SteelEngine { +// pub fn call_function_by_name( +// &mut self, +// function_name: SteelString, +// args: Vec, +// ) -> steel::rvals::Result { +// self.0 +// .call_function_by_name_with_args(function_name.as_str(), args.into_iter().collect()) +// } + +// /// Calling a function that was not defined in the runtime it was created in could +// /// result in panics. You have been warned. +// pub fn call_function( +// &mut self, +// function: SteelVal, +// args: Vec, +// ) -> steel::rvals::Result { +// self.0 +// .call_function_with_args(function, args.into_iter().collect()) +// } + +// pub fn require_module(&mut self, module: SteelString) -> steel::rvals::Result<()> { +// self.0.run(format!("(require \"{}\")", module)).map(|_| ()) +// } +// } + +// impl Custom for SteelEngine {} + +// static ENGINE_ID: AtomicUsize = AtomicUsize::new(0); + +// thread_local! { +// pub static ENGINE_MAP: SteelVal = +// SteelVal::boxed(SteelVal::empty_hashmap()); +// } + +// Low level API work, these need to be loaded into the global environment in a predictable +// location, otherwise callbacks from plugin engines will not be handled properly! +// fn load_engine_api(engine: &mut Engine) { +// fn id_to_engine(value: SteelVal) -> Option { +// if let SteelVal::Boxed(b) = ENGINE_MAP.with(|x| x.clone()) { +// if let SteelVal::HashMapV(h) = b.read().clone() { +// return h.get(&value).cloned(); +// } +// } + +// None +// } + +// // module +// engine +// .register_fn("helix.controller.create-engine", || { +// SteelEngine(configure_engine_impl(Engine::new())) +// }) +// .register_fn("helix.controller.fresh-engine-id", || { +// ENGINE_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst) +// }) +// .register_fn( +// "helix.controller.call-function-by-name", +// SteelEngine::call_function_by_name, +// ) +// .register_fn("helix.controller.call-function", SteelEngine::call_function) +// .register_fn( +// "helix.controller.require-module", +// SteelEngine::require_module, +// ) +// .register_value( +// "helix.controller.engine-map", +// ENGINE_MAP.with(|x| x.clone()), +// ) +// .register_fn("helix.controller.id->engine", id_to_engine); +// } + +fn load_misc_api(engine: &mut Engine, generate_sources: bool) { + let mut module = BuiltInModule::new("helix/core/misc"); + + let mut builtin_misc_module = if generate_sources { + "(require-builtin helix/core/misc as helix.)".to_string() + } else { + "".to_string() + }; + + let mut template_function_arity_0 = |name: &str| { + if generate_sources { + builtin_misc_module.push_str(&format!( + r#" +(provide {}) +(define ({}) + (helix.{} *helix.cx*)) +"#, + name, name, name + )); + } + }; + + // Arity 0 + module.register_fn("hx.cx->pos", cx_pos_within_text); + module.register_fn("mode-switch-old", OnModeSwitchEvent::get_old_mode); + module.register_fn("mode-switch-new", OnModeSwitchEvent::get_new_mode); + + template_function_arity_0("hx.cx->pos"); + + let mut template_function_arity_1 = |name: &str| { + if generate_sources { + builtin_misc_module.push_str(&format!( + r#" +(provide {}) +(define ({} arg) + (helix.{} *helix.cx* arg)) +"#, + name, name, name + )); + } + }; + + // Arity 1 + module.register_fn("hx.custom-insert-newline", custom_insert_newline); + module.register_fn("push-component!", push_component); + module.register_fn("pop-last-component!", pop_last_component_by_name); + module.register_fn("enqueue-thread-local-callback", enqueue_command); + module.register_fn("set-status!", set_status); + + template_function_arity_1("pop-last-component!"); + template_function_arity_1("hx.custom-insert-newline"); + template_function_arity_1("push-component!"); + template_function_arity_1("enqueue-thread-local-callback"); + template_function_arity_1("set-status!"); + + module.register_fn("send-lsp-command", send_arbitrary_lsp_command); + if generate_sources { + builtin_misc_module.push_str( + r#" + (provide send-lsp-command) + ;;@doc + ;; Send an lsp command. The `lsp-name` must correspond to an active lsp. + ;; The method name corresponds to the method name that you'd expect to see + ;; with the lsp, and the params can be passed as a hash table. The callback + ;; provided will be called with whatever result is returned from the LSP, + ;; deserialized from json to a steel value. + ;; + ;; # Example + ;; ```scheme + ;; (define (view-crate-graph) + ;; (send-lsp-command "rust-analyzer" + ;; "rust-analyzer/viewCrateGraph" + ;; (hash "full" #f) + ;; ;; Callback to run with the result + ;; (lambda (result) (displayln result)))) + ;; ``` + (define (send-lsp-command lsp-name method-name params callback) + (helix.send-lsp-command *helix.cx* lsp-name method-name params callback)) + "#, + ); + } + + let mut template_function_arity_2 = |name: &str| { + if generate_sources { + builtin_misc_module.push_str(&format!( + r#" +(provide {}) +(define ({} arg1 arg2) + (helix.{} *helix.cx* arg1 arg2)) +"#, + name, name, name + )); + } + }; + + // Arity 2 + module.register_fn( + "enqueue-thread-local-callback-with-delay", + enqueue_command_with_delay, + ); + + // Arity 2 + module.register_fn("helix-await-callback", await_value); + + template_function_arity_2("enqueue-thread-local-callback-with-delay"); + template_function_arity_2("helix-await-callback"); + + if generate_sources { + let mut target_directory = helix_runtime_search_path(); + + if !target_directory.exists() { + std::fs::create_dir(&target_directory).unwrap(); + } + + target_directory.push("misc.scm"); + + std::fs::write(target_directory, builtin_misc_module).unwrap(); + } + + if generate_sources { + configure_lsp_builtins("misc", &module); + } + + engine.register_module(module); +} + +pub fn helix_runtime_search_path() -> PathBuf { + helix_loader::config_dir().join("helix") +} + +pub fn configure_builtin_sources(engine: &mut Engine, generate_sources: bool) { + load_editor_api(engine, generate_sources); + load_theme_api(engine, generate_sources); + load_configuration_api(engine, generate_sources); + load_typed_commands(engine, generate_sources); + load_static_commands(engine, generate_sources); + // Note: This is going to be completely revamped soon. + load_keymap_api(engine, KeyMapApi::new(), generate_sources); + load_rope_api(engine, generate_sources); + load_misc_api(engine, generate_sources); + load_component_api(engine, generate_sources); + + // TODO: Remove this once all of the globals have been moved into their own modules + if generate_sources { + if std::env::var("STEEL_LSP_HOME").is_err() { + eprintln!("Warning: STEEL_LSP_HOME is not set, so the steel lsp will not be configured with helix primitives"); + } + configure_lsp_globals() + } +} + +fn configure_engine_impl(mut engine: Engine) -> Engine { + log::info!("Loading engine!"); + + engine.add_search_directory(helix_loader::config_dir()); + + engine.register_value("*helix.cx*", SteelVal::Void); + engine.register_value("*helix.config*", SteelVal::Void); + engine.register_value( + "*helix.id*", + SteelVal::IntV(engine.engine_id().as_usize() as _), + ); + + // Don't generate source directories here + configure_builtin_sources(&mut engine, false); + + // Hooks + engine.register_fn("register-hook!", register_hook); + engine.register_fn("log::info!", |message: String| log::info!("{}", message)); + + engine.register_fn("fuzzy-match", |pattern: SteelString, items: SteelVal| { + // Match against how they would be rendered? + + if let SteelVal::ListV(l) = items { + let res = helix_core::fuzzy::fuzzy_match( + pattern.as_str(), + l.iter().filter_map(|x| x.as_string().map(|x| x.as_str())), + false, + ); + + return res + .into_iter() + .map(|x| x.0.to_string().into()) + .collect::>(); + } + + return Vec::new(); + }); + + // Find the workspace + engine.register_fn("helix-find-workspace", || { + helix_core::find_workspace().0.to_str().unwrap().to_string() + }); + + engine.register_fn("doc-id->usize", document_id_to_usize); + + engine.register_fn("new-component!", SteelDynamicComponent::new_dyn); + + engine.register_fn( + "acquire-context-lock", + |callback_fn: SteelVal, place: Option| { + match (&callback_fn, &place) { + (SteelVal::Closure(_), Some(SteelVal::CustomStruct(_))) => {} + _ => { + steel::stop!(TypeMismatch => "acquire-context-lock expected a + callback function and a task object") + } + } + + let rooted = callback_fn.as_rooted(); + let rooted_place = place.map(|x| x.as_rooted()); + + let callback = + move |editor: &mut Editor, _compositor: &mut Compositor, jobs: &mut job::Jobs| { + let mut ctx = Context { + register: None, + count: None, + editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs, + }; + + let cloned_func = rooted.value(); + let cloned_place = rooted_place.as_ref().map(|x| x.value()); + + enter_engine(|guard| { + if let Err(e) = guard + .with_mut_reference::(&mut ctx) + // Block until the other thread is finished in its critical + // section... + .consume(move |engine, args| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + + if let Some(SteelVal::CustomStruct(s)) = cloned_place { + let mutex = s.get_mut_index(0).unwrap(); + mutex_lock(&mutex).unwrap(); + } + + // Acquire lock, wait until its done + let result = + engine.call_function_with_args(cloned_func.clone(), Vec::new()); + + if let Some(SteelVal::CustomStruct(s)) = cloned_place { + match result { + Ok(result) => { + // Store the result of the callback so that the + // next downstream user can handle it. + s.set_index(2, result); + s.set_index(1, SteelVal::BoolV(true)); + let mutex = s.get_mut_index(0).unwrap(); + mutex_unlock(&mutex).unwrap(); + } + + Err(e) => { + return Err(e); + } + } + } + + Ok(()) + }) + { + present_error_inside_engine_context(&mut ctx, guard, e); + } + }) + }; + job::dispatch_blocking_jobs(callback); + + Ok(()) + }, + ); + + engine.register_fn("SteelDynamicComponent?", |object: SteelVal| { + if let SteelVal::Custom(v) = object { + if let Some(wrapped) = v.read().as_any_ref().downcast_ref::() { + return wrapped.inner.as_any().is::(); + } else { + false + } + } else { + false + } + }); + + engine.register_fn( + "prompt", + |prompt: String, callback_fn: SteelVal| -> WrappedDynComponent { + let callback_fn_guard = callback_fn.as_rooted(); + + let prompt = Prompt::new( + prompt.into(), + None, + |_, _| Vec::new(), + move |cx, input, prompt_event| { + log::info!("Calling dynamic prompt callback"); + + if prompt_event != PromptEvent::Validate { + return; + } + + let mut ctx = Context { + register: None, + count: None, + editor: cx.editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs: cx.jobs, + }; + + let cloned_func = callback_fn_guard.value(); + + with_interrupt_handler(|| { + enter_engine(|guard| { + if let Err(e) = guard + .with_mut_reference::(&mut ctx) + .consume(move |engine, args| { + let context = args[0].clone(); + + engine.update_value("*helix.cx*", context); + + engine.call_function_with_args( + cloned_func.clone(), + vec![input.into_steelval().unwrap()], + ) + }) + { + present_error_inside_engine_context(&mut ctx, guard, e); + } + }) + }) + }, + ); + + WrappedDynComponent { + inner: Some(Box::new(prompt)), + } + }, + ); + + engine.register_fn("picker", |values: Vec| -> WrappedDynComponent { + let columns = [PickerColumn::new( + "path", + |item: &PathBuf, root: &PathBuf| { + item.strip_prefix(root) + .unwrap_or(item) + .to_string_lossy() + .into() + }, + )]; + let cwd = helix_stdx::env::current_working_dir(); + + let picker = ui::Picker::new(columns, 0, [], cwd, move |cx, path: &PathBuf, action| { + if let Err(e) = cx.editor.open(path, action) { + let err = if let Some(err) = e.source() { + format!("{}", err) + } else { + format!("unable to open \"{}\"", path.display()) + }; + cx.editor.set_error(err); + } + }) + .with_preview(|_editor, path| Some((PathOrId::Path(path), None))); + + let injector = picker.injector(); + + for file in values { + if injector.push(PathBuf::from(file)).is_err() { + break; + } + } + + WrappedDynComponent { + inner: Some(Box::new(ui::overlay::overlaid(picker))), + } + }); + + engine.register_fn("Component::Text", |contents: String| WrappedDynComponent { + inner: Some(Box::new(crate::ui::Text::new(contents))), + }); + + // Create directory since we can't do that in the current state + engine.register_fn("hx.create-directory", create_directory); + + engine +} + +fn get_highlighted_text(cx: &mut Context) -> String { + let (view, doc) = current_ref!(cx.editor); + let text = doc.text().slice(..); + doc.selection(view.id).primary().slice(text).to_string() +} + +fn current_selection(cx: &mut Context) -> Selection { + let (view, doc) = current_ref!(cx.editor); + doc.selection(view.id).clone() +} + +fn set_selection(cx: &mut Context, selection: Selection) { + let (view, doc) = current!(cx.editor); + doc.set_selection(view.id, selection) +} + +fn current_line_number(cx: &mut Context) -> usize { + let (view, doc) = current_ref!(cx.editor); + helix_core::coords_at_pos( + doc.text().slice(..), + doc.selection(view.id) + .primary() + .cursor(doc.text().slice(..)), + ) + .row +} + +fn get_selection(cx: &mut Context) -> String { + let (view, doc) = current_ref!(cx.editor); + let text = doc.text().slice(..); + + let grapheme_start = doc.selection(view.id).primary().cursor(text); + let grapheme_end = graphemes::next_grapheme_boundary(text, grapheme_start); + + if grapheme_start == grapheme_end { + return "".into(); + } + + let grapheme = text.slice(grapheme_start..grapheme_end).to_string(); + + let printable = grapheme.chars().fold(String::new(), |mut s, c| { + match c { + '\0' => s.push_str("\\0"), + '\t' => s.push_str("\\t"), + '\n' => s.push_str("\\n"), + '\r' => s.push_str("\\r"), + _ => s.push(c), + } + + s + }); + + printable +} + +// TODO: Replace with eval-string +pub fn run_expression_in_engine(cx: &mut Context, text: String) -> anyhow::Result<()> { + let callback = async move { + let call: Box = Box::new( + move |editor: &mut Editor, compositor: &mut Compositor, jobs: &mut job::Jobs| { + let mut ctx = Context { + register: None, + count: None, + editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs, + }; + + let output = enter_engine(|guard| { + guard + .with_mut_reference::(&mut ctx) + .consume(move |engine, args| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + + engine.compile_and_run_raw_program(text.clone()) + }) + }); + + match output { + Ok(output) => { + let (output, _success) = (Tendril::from(format!("{:?}", output)), true); + + let contents = ui::Markdown::new( + format!("```\n{}\n```", output), + editor.syn_loader.clone(), + ); + let popup = Popup::new("engine", contents).position(Some( + helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2), + )); + compositor.replace_or_push("engine", popup); + } + Err(e) => enter_engine(|x| present_error_inside_engine_context(&mut ctx, x, e)), + } + }, + ); + Ok(call) + }; + cx.jobs.local_callback(callback); + + Ok(()) +} + +pub fn load_buffer(cx: &mut Context) -> anyhow::Result<()> { + let (text, path) = { + let (_, doc) = current!(cx.editor); + + let text = doc.text().to_string(); + let path = current_path(cx); + + (text, path) + }; + + let callback = async move { + let call: Box = Box::new( + move |editor: &mut Editor, compositor: &mut Compositor, jobs: &mut job::Jobs| { + let mut ctx = Context { + register: None, + count: None, + editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs, + }; + + let output = enter_engine(|guard| { + guard + .with_mut_reference::(&mut ctx) + .consume(move |engine, args| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + + match path.clone() { + Some(path) => engine.compile_and_run_raw_program_with_path( + // TODO: Figure out why I have to clone this text here. + text.clone(), + PathBuf::from(path), + ), + None => engine.compile_and_run_raw_program(text.clone()), + } + }) + }); + + match output { + Ok(output) => { + let (output, _success) = (Tendril::from(format!("{:?}", output)), true); + + let contents = ui::Markdown::new( + format!("```\n{}\n```", output), + editor.syn_loader.clone(), + ); + let popup = Popup::new("engine", contents).position(Some( + helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2), + )); + compositor.replace_or_push("engine", popup); + } + Err(e) => enter_engine(|x| present_error_inside_engine_context(&mut ctx, x, e)), + } + }, + ); + Ok(call) + }; + cx.jobs.local_callback(callback); + + Ok(()) +} + +fn get_helix_scm_path() -> String { + helix_module_file().to_str().unwrap().to_string() +} + +fn get_init_scm_path() -> String { + steel_init_file().to_str().unwrap().to_string() +} + +/// Get the current path! See if this can be done _without_ this function? +// TODO: +fn current_path(cx: &mut Context) -> Option { + let current_focus = cx.editor.tree.focus; + let view = cx.editor.tree.get(current_focus); + let doc = &view.doc; + // Lifetime of this needs to be tied to the existing document + let current_doc = cx.editor.documents.get(doc); + current_doc.and_then(|x| x.path().and_then(|x| x.to_str().map(|x| x.to_string()))) +} + +fn set_scratch_buffer_name(cx: &mut Context, name: String) { + let current_focus = cx.editor.tree.focus; + let view = cx.editor.tree.get(current_focus); + let doc = &view.doc; + // Lifetime of this needs to be tied to the existing document + let current_doc = cx.editor.documents.get_mut(doc); + + if let Some(current_doc) = current_doc { + current_doc.name = Some(name); + } +} + +fn cx_current_focus(cx: &mut Context) -> helix_view::ViewId { + cx.editor.tree.focus +} + +fn cx_get_document_id(cx: &mut Context, view_id: helix_view::ViewId) -> DocumentId { + cx.editor.tree.get(view_id).doc +} + +fn document_id_to_text(cx: &mut Context, doc_id: DocumentId) -> Option { + cx.editor + .documents + .get(&doc_id) + .map(|x| SteelRopeSlice::new(x.text().clone())) +} + +fn cx_is_document_in_view(cx: &mut Context, doc_id: DocumentId) -> Option { + cx.editor + .tree + .traverse() + .find(|(_, v)| v.doc == doc_id) + .map(|(id, _)| id) +} + +fn cx_document_exists(cx: &mut Context, doc_id: DocumentId) -> bool { + cx.editor.documents.get(&doc_id).is_some() +} + +fn document_path(cx: &mut Context, doc_id: DocumentId) -> Option { + cx.editor + .documents + .get(&doc_id) + .and_then(|doc| doc.path().and_then(|x| x.to_str()).map(|x| x.to_string())) +} + +fn cx_editor_all_documents(cx: &mut Context) -> Vec { + cx.editor.documents.keys().copied().collect() +} + +fn cx_switch(cx: &mut Context, doc_id: DocumentId) { + cx.editor.switch(doc_id, Action::VerticalSplit) +} + +fn cx_switch_action(cx: &mut Context, doc_id: DocumentId, action: Action) { + cx.editor.switch(doc_id, action) +} + +fn cx_get_mode(cx: &mut Context) -> Mode { + cx.editor.mode +} + +fn cx_set_mode(cx: &mut Context, mode: Mode) { + cx.editor.mode = mode +} + +// Overlay the dynamic component, see what happens? +// Probably need to pin the values to this thread - wrap it in a shim which pins the value +// to this thread? - call methods on the thread local value? +fn push_component(cx: &mut Context, component: &mut WrappedDynComponent) { + log::info!("Pushing dynamic component!"); + + let inner = component.inner.take().unwrap(); + + let callback = async move { + let call: Box = Box::new( + move |_editor: &mut Editor, compositor: &mut Compositor, _| compositor.push(inner), + ); + Ok(call) + }; + cx.jobs.local_callback(callback); +} + +fn pop_last_component_by_name(cx: &mut Context, name: SteelString) { + let callback = async move { + let call: Box = Box::new( + move |_editor: &mut Editor, compositor: &mut Compositor, _jobs: &mut job::Jobs| { + compositor.remove_by_dynamic_name(&name); + }, + ); + Ok(call) + }; + cx.jobs.local_callback(callback); +} + +fn set_status(cx: &mut Context, value: SteelVal) { + cx.editor.set_status(value.to_string()) +} + +fn enqueue_command(cx: &mut Context, callback_fn: SteelVal) { + let rooted = callback_fn.as_rooted(); + + let callback = async move { + let call: Box = Box::new( + move |editor: &mut Editor, _compositor: &mut Compositor, jobs: &mut job::Jobs| { + let mut ctx = Context { + register: None, + count: None, + editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs, + }; + + let cloned_func = rooted.value(); + + enter_engine(|guard| { + if let Err(e) = guard + .with_mut_reference::(&mut ctx) + .consume(move |engine, args| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + + engine.call_function_with_args(cloned_func.clone(), Vec::new()) + }) + { + present_error_inside_engine_context(&mut ctx, guard, e); + } + }) + }, + ); + Ok(call) + }; + cx.jobs.local_callback(callback); +} + +// Apply arbitrary delay for update rate... +fn enqueue_command_with_delay(cx: &mut Context, delay: SteelVal, callback_fn: SteelVal) { + let rooted = callback_fn.as_rooted(); + + let callback = async move { + let delay = delay.int_or_else(|| panic!("FIX ME")).unwrap(); + + tokio::time::sleep(tokio::time::Duration::from_millis(delay as u64)).await; + + let call: Box = Box::new( + move |editor: &mut Editor, _compositor: &mut Compositor, jobs: &mut job::Jobs| { + let mut ctx = Context { + register: None, + count: None, + editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs, + }; + + let cloned_func = rooted.value(); + + enter_engine(|guard| { + if let Err(e) = guard + .with_mut_reference::(&mut ctx) + .consume(move |engine, args| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + + engine.call_function_with_args(cloned_func.clone(), Vec::new()) + }) + { + present_error_inside_engine_context(&mut ctx, guard, e); + } + }) + }, + ); + Ok(call) + }; + cx.jobs.local_callback(callback); +} + +// value _must_ be a future here. Otherwise awaiting will cause problems! +fn await_value(cx: &mut Context, value: SteelVal, callback_fn: SteelVal) { + if !value.is_future() { + return; + } + + let rooted = callback_fn.as_rooted(); + + let callback = async move { + let future_value = value.as_future().unwrap().await; + + let call: Box = Box::new( + move |editor: &mut Editor, _compositor: &mut Compositor, jobs: &mut job::Jobs| { + let mut ctx = Context { + register: None, + count: None, + editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs, + }; + + let cloned_func = rooted.value(); + + match future_value { + Ok(inner) => { + let callback = move |engine: &mut Engine, args: Vec| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + + // args.push(inner); + engine.call_function_with_args(cloned_func.clone(), vec![inner]) + }; + + enter_engine(|guard| { + if let Err(e) = guard + .with_mut_reference::(&mut ctx) + .consume_once(callback) + { + present_error_inside_engine_context(&mut ctx, guard, e); + } + }) + } + Err(e) => enter_engine(|x| present_error_inside_engine_context(&mut ctx, x, e)), + } + }, + ); + Ok(call) + }; + cx.jobs.local_callback(callback); +} +// Check that we successfully created a directory? +fn create_directory(path: String) { + let path = helix_stdx::path::canonicalize(&PathBuf::from(path)); + + if path.exists() { + return; + } else { + std::fs::create_dir(path).unwrap(); + } +} + +pub fn cx_pos_within_text(cx: &mut Context) -> usize { + let (view, doc) = current_ref!(cx.editor); + + let text = doc.text().slice(..); + + let selection = doc.selection(view.id).clone(); + + let pos = selection.primary().cursor(text); + + pos +} + +pub fn get_helix_cwd(_cx: &mut Context) -> Option { + helix_stdx::env::current_working_dir() + .as_os_str() + .to_str() + .map(|x| x.into()) +} + +// Special newline... +pub fn custom_insert_newline(cx: &mut Context, indent: String) { + let (view, doc) = current_ref!(cx.editor); + + // let rope = doc.text().clone(); + + let text = doc.text().slice(..); + + let contents = doc.text(); + let selection = doc.selection(view.id).clone(); + let mut ranges = helix_core::SmallVec::with_capacity(selection.len()); + + // TODO: this is annoying, but we need to do it to properly calculate pos after edits + let mut global_offs = 0; + + let mut transaction = + helix_core::Transaction::change_by_selection(contents, &selection, |range| { + let pos = range.cursor(text); + + let prev = if pos == 0 { + ' ' + } else { + contents.char(pos - 1) + }; + let curr = contents.get_char(pos).unwrap_or(' '); + + let current_line = text.char_to_line(pos); + let line_is_only_whitespace = text + .line(current_line) + .chars() + .all(|char| char.is_ascii_whitespace()); + + let mut new_text = String::new(); + + // If the current line is all whitespace, insert a line ending at the beginning of + // the current line. This makes the current line empty and the new line contain the + // indentation of the old line. + let (from, to, local_offs) = if line_is_only_whitespace { + let line_start = text.line_to_char(current_line); + new_text.push_str(doc.line_ending.as_str()); + + (line_start, line_start, new_text.chars().count()) + } else { + // If we are between pairs (such as brackets), we want to + // insert an additional line which is indented one level + // more and place the cursor there + let on_auto_pair = doc + .auto_pairs(cx.editor) + .and_then(|pairs| pairs.get(prev)) + .map_or(false, |pair| pair.open == prev && pair.close == curr); + + let local_offs = if on_auto_pair { + let inner_indent = indent.clone() + doc.indent_style.as_str(); + new_text.reserve_exact(2 + indent.len() + inner_indent.len()); + new_text.push_str(doc.line_ending.as_str()); + new_text.push_str(&inner_indent); + let local_offs = new_text.chars().count(); + new_text.push_str(doc.line_ending.as_str()); + new_text.push_str(&indent); + local_offs + } else { + new_text.reserve_exact(1 + indent.len()); + new_text.push_str(doc.line_ending.as_str()); + new_text.push_str(&indent); + new_text.chars().count() + }; + + (pos, pos, local_offs) + }; + + let new_range = if doc.restore_cursor { + // when appending, extend the range by local_offs + Range::new( + range.anchor + global_offs, + range.head + local_offs + global_offs, + ) + } else { + // when inserting, slide the range by local_offs + Range::new( + range.anchor + local_offs + global_offs, + range.head + local_offs + global_offs, + ) + }; + + // TODO: range replace or extend + // range.replace(|range| range.is_empty(), head); -> fn extend if cond true, new head pos + // can be used with cx.mode to do replace or extend on most changes + ranges.push(new_range); + global_offs += new_text.chars().count(); + + (from, to, Some(new_text.into())) + }); + + transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index())); + + let (view, doc) = current!(cx.editor); + doc.apply(&transaction, view.id); +} + +// fn search_in_directory(cx: &mut Context, directory: String) { +// let buf = PathBuf::from(directory); +// let search_path = expand_tilde(&buf); +// let path = search_path.to_path_buf(); +// crate::commands::search_in_directory(cx, path); +// } + +// TODO: Result should create unrecoverable result, and should have a special +// recoverable result - that way we can handle both, not one in particular +fn regex_selection(cx: &mut Context, regex: String) { + if let Ok(regex) = helix_stdx::rope::Regex::new(®ex) { + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + if let Some(selection) = + helix_core::selection::select_on_matches(text, doc.selection(view.id), ®ex) + { + doc.set_selection(view.id, selection); + } + } +} + +fn replace_selection(cx: &mut Context, value: String) { + let (view, doc) = current!(cx.editor); + + let selection = doc.selection(view.id); + let transaction = + helix_core::Transaction::change_by_selection(doc.text(), selection, |range| { + if !range.is_empty() { + (range.from(), range.to(), Some(value.to_owned().into())) + } else { + (range.from(), range.to(), None) + } + }); + + doc.apply(&transaction, view.id); +} + +// TODO: Remove this! +fn move_window_to_the_left(cx: &mut Context) { + while cx + .editor + .tree + .swap_split_in_direction(helix_view::tree::Direction::Left) + .is_some() + {} +} + +// TODO: Remove this! +fn move_window_to_the_right(cx: &mut Context) { + while cx + .editor + .tree + .swap_split_in_direction(helix_view::tree::Direction::Right) + .is_some() + {} +} + +fn send_arbitrary_lsp_command( + cx: &mut Context, + name: SteelString, + command: SteelString, + // Arguments - these will be converted to some json stuff + json_argument: Option, + callback_fn: SteelVal, +) -> anyhow::Result<()> { + let argument = json_argument.map(|x| serde_json::Value::try_from(x).unwrap()); + + let (_view, doc) = current!(cx.editor); + + let language_server_id = anyhow::Context::context( + doc.language_servers().find(|x| x.name() == name.as_str()), + "Unable to find the language server specified!", + )? + .id(); + + let future = match cx + .editor + .language_server_by_id(language_server_id) + .and_then(|language_server| { + language_server.non_standard_extension(command.to_string(), argument) + }) { + Some(future) => future, + None => { + // TODO: Come up with a better message once we check the capabilities for + // the arbitrary thing you're trying to do, since for now the above actually + // always returns a `Some` + cx.editor.set_error( + "Language server does not support whatever command you just tried to do", + ); + return Ok(()); + } + }; + + let rooted = callback_fn.as_rooted(); + + create_callback(cx, future, rooted)?; + + Ok(()) +} + +fn create_callback + 'static>( + cx: &mut Context, + future: impl std::future::Future> + 'static, + rooted: steel::RootedSteelVal, +) -> Result<(), anyhow::Error> { + let callback = async move { + // Result of the future - this will be whatever we get back + // from the lsp call + let res = future.await?; + + let call: Box = Box::new( + move |editor: &mut Editor, _compositor: &mut Compositor, jobs: &mut job::Jobs| { + let mut ctx = Context { + register: None, + count: None, + editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs, + }; + + let cloned_func = rooted.value(); + + enter_engine(move |guard| match TryInto::::try_into(res) { + Ok(result) => { + if let Err(e) = guard + .with_mut_reference::(&mut ctx) + .consume(move |engine, args| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + + engine.call_function_with_args( + cloned_func.clone(), + vec![result.clone()], + ) + }) + { + present_error_inside_engine_context(&mut ctx, guard, e); + } + } + Err(e) => { + present_error_inside_engine_context(&mut ctx, guard, e); + } + }) + }, + ); + Ok(call) + }; + cx.jobs.local_callback(callback); + Ok(()) +} diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 68ba9bab556e..f96b9e8c5e5c 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::Deref; @@ -14,6 +13,8 @@ use helix_view::editor::{CloseError, ConfigEvent}; use serde_json::Value; use ui::completers::{self, Completer}; +use std::fmt::Write; + #[derive(Clone)] pub struct TypableCommand { pub name: &'static str, @@ -44,21 +45,21 @@ pub struct CommandSignature { } impl CommandSignature { - 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, @@ -651,6 +652,8 @@ pub(super) fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> let (modified_ids, modified_names): (Vec<_>, 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(), doc.display_name())) .unzip(); if let Some(first) = modified_ids.first() { @@ -690,7 +693,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 write_scratch { errors.push("cannot write a buffer without a filename"); } @@ -879,21 +888,42 @@ fn theme( // Ensures that a preview theme gets cleaned up if the user backspaces until the prompt is empty. cx.editor.unset_theme_preview(); } else if let Some(theme_name) = args.first() { - if let Ok(theme) = cx.editor.theme_loader.load(theme_name) { + // if let Ok(theme) = cx.editor.theme_loader.load(theme_name) { + // if !(true_color || theme.is_16_color()) { + // bail!("Unsupported theme: theme requires true color support"); + // } + // cx.editor.set_theme_preview(theme); + // }; + + if let Ok(theme) = cx.editor.theme_loader.load(theme_name).or_else(|_| { + cx.editor + .user_defined_themes + .get(theme_name.as_ref()) + .ok_or_else(|| anyhow::anyhow!("Could not load theme")) + .cloned() + }) { if !(true_color || theme.is_16_color()) { bail!("Unsupported theme: theme requires true color support"); } cx.editor.set_theme_preview(theme); - }; + } }; } PromptEvent::Validate => { if let Some(theme_name) = args.first() { - let theme = cx - .editor - .theme_loader - .load(theme_name) - .map_err(|err| anyhow::anyhow!("Could not load theme: {}", err))?; + let theme = cx.editor.theme_loader.load(theme_name).or_else(|_| { + cx.editor + .user_defined_themes + .get(theme_name.as_ref()) + .ok_or_else(|| anyhow::anyhow!("Could not load theme")) + .cloned() + })?; + + // let theme = cx + // .editor + // .theme_loader + // .load(theme_name) + // .map_err(|err| anyhow::anyhow!("Could not load theme: {}", err))?; if !(true_color || theme.is_16_color()) { bail!("Unsupported theme: theme requires true color support"); } @@ -2282,6 +2312,39 @@ fn pipe_impl( Ok(()) } +fn run_shell_command_text( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + let shell = cx.editor.config().shell.clone(); + let args = args.join(" "); + + let callback = async move { + let output = shell_impl_async(&shell, &args, None).await?; + let call: job::Callback = Callback::EditorCompositor(Box::new( + move |editor: &mut Editor, compositor: &mut Compositor| { + if !output.is_empty() { + let contents = ui::Text::new(format!("{}", output)); + let popup = Popup::new("shell", contents).position(Some( + helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2), + )); + compositor.replace_or_push("shell", popup); + } + editor.set_status("Command succeeded"); + }, + )); + Ok(call) + }; + cx.jobs.callback(callback); + + Ok(()) +} + fn run_shell_command( cx: &mut compositor::Context, args: &[Cow], @@ -2631,7 +2694,9 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ aliases: &["n"], doc: "Create a new scratch buffer.", fun: new_file, - signature: CommandSignature::none(), + // TODO: This seems to complete with a filename, but doesn't use that filename to + // set the path of the newly created buffer. + signature: CommandSignature::positional(&[completers::filename]), }, TypableCommand { name: "format", @@ -3099,6 +3164,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: run_shell_command, signature: CommandSignature::all(completers::filename) }, + TypableCommand { + name: "run-shell-command-text", + aliases: &["sh"], + doc: "Run a shell command", + fun: run_shell_command_text, + signature: CommandSignature::all(completers::filename) + }, TypableCommand { name: "reset-diff-change", aliases: &["diffget", "diffg"], @@ -3166,7 +3238,10 @@ pub(super) fn command_mode(cx: &mut Context) { if words.is_empty() || (words.len() == 1 && !shellwords.ends_with_whitespace()) { fuzzy_match( input, - TYPABLE_COMMAND_LIST.iter().map(|command| command.name), + TYPABLE_COMMAND_LIST + .iter() + .map(|command| Cow::from(command.name)) + .chain(crate::commands::engine::ScriptingEngine::available_commands()), false, ) .into_iter() @@ -3217,14 +3292,64 @@ pub(super) fn command_mode(cx: &mut Context) { return; } + // TODO: @Matt - Add completion for added scripting commands here // Handle typable commands + + // Register callback functions here - if the prompt event is validate, + // Grab the function run and run through the hooks. if let Some(cmd) = typed::TYPABLE_COMMAND_MAP.get(parts[0]) { let shellwords = Shellwords::from(input); let args = shellwords.words(); - if let Err(e) = (cmd.fun)(cx, &args[1..], event) { cx.editor.set_error(format!("{}", e)); } + + if event == PromptEvent::Validate { + let mappable_command = MappableCommand::Typable { + name: cmd.name.to_string(), + args: Vec::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, + }); + } + } else if ScriptingEngine::call_typed_command(cx, input, &parts, event) { + // Engine handles the other cases + if event == PromptEvent::Validate { + let mappable_command = MappableCommand::Typable { + name: input.to_string(), + args: Vec::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, + }); + } } else if event == PromptEvent::Validate { cx.editor .set_error(format!("no such command: '{}'", parts[0])); @@ -3241,6 +3366,8 @@ pub(super) fn command_mode(cx: &mut Context) { return Some((*doc).into()); } return Some(format!("{}\nAliases: {}", doc, aliases.join(", ")).into()); + } else if let Some(doc) = ScriptingEngine::get_doc_for_identifier(part) { + return Some(doc.into()); } None diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 3dcb5f2bfcbc..fbaffc5c22f5 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -13,6 +13,7 @@ pub type SyncCallback = Box; pub enum EventResult { Ignored(Option), Consumed(Option), + ConsumedWithoutRerender, } use crate::job::Jobs; @@ -73,6 +74,10 @@ pub trait Component: Any + AnyComponent { fn id(&self) -> Option<&'static str> { None } + + fn name(&self) -> Option<&str> { + None + } } pub struct Compositor { @@ -136,6 +141,14 @@ impl Compositor { Some(self.layers.remove(idx)) } + pub fn remove_by_dynamic_name(&mut self, id: &str) -> Option> { + let idx = self + .layers + .iter() + .position(|layer| layer.name() == Some(id))?; + Some(self.layers.remove(idx)) + } + pub fn handle_event(&mut self, event: &Event, cx: &mut Context) -> bool { // If it is a key event and a macro is being recorded, push the key event to the recording. if let (Event::Key(key), Some((_, keys))) = (event, &mut cx.editor.macro_recording) { @@ -159,6 +172,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); } @@ -175,7 +192,9 @@ impl Compositor { pub fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { for layer in &mut self.layers { - layer.render(area, surface, cx); + if layer.should_update() { + layer.render(area, surface, cx) + }; } } diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index bcba8d8e1d45..a0873c16cd0f 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -64,6 +64,7 @@ impl Config { global.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig)); let local_config: Result = local.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig)); + let res = match (global_config, local_config) { (Ok(global), Ok(local)) => { let mut keys = keymap::default(); @@ -100,6 +101,7 @@ impl Config { if let Some(keymap) = config.keys { merge_keys(&mut keys, keymap); } + Config { theme: config.theme, keys, @@ -122,6 +124,7 @@ impl Config { fs::read_to_string(helix_loader::config_file()).map_err(ConfigLoadError::Error); let local_config = fs::read_to_string(helix_loader::workspace_config_file()) .map_err(ConfigLoadError::Error); + Config::load(global_config, local_config) } } diff --git a/helix-term/src/job.rs b/helix-term/src/job.rs index 72ed892ddf9a..2b9da4c29127 100644 --- a/helix-term/src/job.rs +++ b/helix-term/src/job.rs @@ -5,13 +5,19 @@ use once_cell::sync::OnceCell; use crate::compositor::Compositor; +use futures_util::future::LocalBoxFuture; use futures_util::future::{BoxFuture, Future, FutureExt}; use futures_util::stream::{FuturesUnordered, StreamExt}; use tokio::sync::mpsc::{channel, Receiver, Sender}; +pub type EditorCompositorJobsCallback = + Box; pub type EditorCompositorCallback = Box; pub type EditorCallback = Box; +pub type ThreadLocalEditorCompositorCallback = + Box; + runtime_local! { static JOB_QUEUE: OnceCell> = OnceCell::new(); } @@ -32,7 +38,15 @@ pub fn dispatch_blocking(job: impl FnOnce(&mut Editor, &mut Compositor) + Send + send_blocking(jobs, Callback::EditorCompositor(Box::new(job))) } +pub fn dispatch_blocking_jobs( + job: impl FnOnce(&mut Editor, &mut Compositor, &mut Jobs) + Send + 'static, +) { + let jobs = JOB_QUEUE.wait(); + send_blocking(jobs, Callback::EditorCompositorJobs(Box::new(job))) +} + pub enum Callback { + EditorCompositorJobs(EditorCompositorJobsCallback), EditorCompositor(EditorCompositorCallback), Editor(EditorCallback), } @@ -45,9 +59,13 @@ pub struct Job { pub wait: bool, } +pub type ThreadLocalJob = + LocalBoxFuture<'static, anyhow::Result>>; + pub struct Jobs { - /// jobs that need to complete before we exit. + /// jobs the ones that need to complete before we exit. pub wait_futures: FuturesUnordered, + pub local_futures: FuturesUnordered, pub callbacks: Receiver, pub status_messages: Receiver, } @@ -83,6 +101,7 @@ impl Jobs { let status_messages = helix_event::status::setup(); Self { wait_futures: FuturesUnordered::new(), + local_futures: FuturesUnordered::new(), callbacks: rx, status_messages, } @@ -99,8 +118,18 @@ impl Jobs { self.add(Job::with_callback(f)); } + pub fn local_callback< + F: Future> + 'static, + >( + &mut self, + f: F, + ) { + self.local_futures + .push(f.map(|r| r.map(Some)).boxed_local()); + } + pub fn handle_callback( - &self, + &mut self, editor: &mut Editor, compositor: &mut Compositor, call: anyhow::Result>, @@ -108,6 +137,7 @@ impl Jobs { match call { Ok(None) => {} Ok(Some(call)) => match call { + Callback::EditorCompositorJobs(call) => call(editor, compositor, self), Callback::EditorCompositor(call) => call(editor, compositor), Callback::Editor(call) => call(editor), }, @@ -117,6 +147,21 @@ impl Jobs { } } + pub fn handle_local_callback( + &mut self, + editor: &mut Editor, + compositor: &mut Compositor, + call: anyhow::Result>, + ) { + match call { + Ok(None) => {} + Ok(Some(call)) => call(editor, compositor, self), + Err(e) => { + editor.set_error(format!("Sync job failed: {}", e)); + } + } + } + pub fn add(&self, j: Job) { if j.wait { self.wait_futures.push(j.future); diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 020ecaf40f0f..81001ba01d3a 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -235,6 +235,23 @@ impl KeyTrie { res } + pub fn apply(&mut self, func: &mut dyn FnMut(&mut MappableCommand)) { + match self { + KeyTrie::MappableCommand(MappableCommand::Macro { .. }) => {} + KeyTrie::MappableCommand(cmd) => (func)(cmd), + KeyTrie::Node(next) => { + for (_, trie) in &mut next.map { + trie.apply(func); + } + } + KeyTrie::Sequence(seq) => { + for s in seq { + (func)(s) + } + } + }; + } + pub fn node(&self) -> Option<&KeyTrieNode> { match *self { KeyTrie::Node(ref node) => Some(node), @@ -326,12 +343,14 @@ impl Keymaps { .is_some_and(|node| node.contains_key(&key)) } - /// Lookup `key` in the keymap to try and find a command to execute. Escape - /// key cancels pending keystrokes. If there are no pending keystrokes but a - /// sticky node is in use, it will be cleared. - pub fn get(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult { + pub(crate) fn get_with_map( + &mut self, + keymaps: &HashMap, + mode: Mode, + key: KeyEvent, + ) -> KeymapResult { // TODO: remove the sticky part and look up manually - let keymaps = &*self.map(); + // let keymaps = &*self.map(); let keymap = &keymaps[&mode]; if key!(Esc) == key { @@ -379,6 +398,13 @@ impl Keymaps { None => KeymapResult::Cancelled(self.state.drain(..).collect()), } } + + /// Lookup `key` in the keymap to try and find a command to execute. Escape + /// key cancels pending keystrokes. If there are no pending keystrokes but a + /// sticky node is in use, it will be cleared. + pub fn get(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult { + self.get_with_map(&*self.map(), mode, key) + } } impl Default for Keymaps { diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index a3a27a07626a..7a27a96754f4 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -116,6 +116,9 @@ FLAGS: setup_logging(args.verbosity).context("failed to initialize logging")?; + // Initialize the engine before we boot up! + helix_term::commands::ScriptingEngine::initialize(); + // Before setting the working directory, resolve all the paths in args.files for (path, _) in &mut args.files { *path = helix_stdx::path::canonicalize(&*path); diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 14397bb5c4c7..492a74fc1f21 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -278,16 +278,30 @@ impl Completion { // always present here let mut item = item.unwrap().clone(); - let language_server = language_server!(item); - let offset_encoding = language_server.offset_encoding(); + // let language_server = language_server!(item); + // let offset_encoding = language_server.offset_encoding(); + + let mut language_server_option = None; if !item.resolved { + let language_server = language_server!(item); + // let offset_encoding = language_server.offset_encoding(); + if let Some(resolved) = Self::resolve_completion_item(language_server, item.item.clone()) { item.item = resolved; } + + language_server_option = Some(language_server); }; + + // let language_server = language_server!(item); + let offset_encoding = language_server_option + .as_ref() + .map(|x| x.offset_encoding()) + .unwrap_or_default(); + // if more text was entered, remove it doc.restore(view, &savepoint, true); // save an undo checkpoint before the completion diff --git a/helix-term/src/ui/document.rs b/helix-term/src/ui/document.rs index ae00ea149490..0f848d7dc097 100644 --- a/helix-term/src/ui/document.rs +++ b/helix-term/src/ui/document.rs @@ -70,6 +70,7 @@ impl> Iterator for StyleIter<'_, H> { } } + #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub struct LinePos { /// Indicates whether the given visual line @@ -100,6 +101,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 f7541fe25750..ac4ee8e4dd14 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -1,5 +1,5 @@ use crate::{ - commands::{self, OnKeyCallback}, + commands::{self, engine::ScriptingEngine, OnKeyCallback}, compositor::{Component, Context, Event, EventResult}, events::{OnModeSwitch, PostCommand}, key, @@ -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| { @@ -1486,6 +1490,22 @@ impl Component for EditorView { _ => false, }; + let mut area = area; + + // TODO: This may need to get looked at! + if let Some(top) = cx.editor.editor_clipping.top { + area = area.clip_top(top); + } + if let Some(bottom) = cx.editor.editor_clipping.bottom { + area = area.clip_bottom(bottom); + } + if let Some(left) = cx.editor.editor_clipping.left { + area = area.clip_left(left); + } + if let Some(right) = cx.editor.editor_clipping.right { + area = area.clip_right(right); + } + // -1 for commandline and -1 for bufferline let mut editor_area = area.clip_bottom(1); if use_bufferline { diff --git a/helix-term/src/ui/extension.rs b/helix-term/src/ui/extension.rs new file mode 100644 index 000000000000..71552c433fd1 --- /dev/null +++ b/helix-term/src/ui/extension.rs @@ -0,0 +1,11 @@ +#[cfg(feature = "steel")] +mod steel_implementations { + + use crate::{ + compositor::Component, + ui::{Popup, Text}, + }; + + impl steel::rvals::Custom for Text {} + impl steel::rvals::Custom for Popup {} +} diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 6a3e198c1051..a49c0375120d 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; @@ -294,13 +295,18 @@ pub mod completers { .collect() } - pub fn theme(_editor: &Editor, input: &str) -> Vec { + pub fn theme(editor: &Editor, input: &str) -> Vec { let mut names = theme::Loader::read_names(&helix_loader::config_dir().join("themes")); for rt_dir in helix_loader::runtime_dirs() { names.extend(theme::Loader::read_names(&rt_dir.join("themes"))); } + names.push("default".into()); names.push("base16_default".into()); + + // Include any user defined themes as well + names.extend(editor.user_defined_themes.keys().map(|x| x.into())); + names.sort(); names.dedup(); diff --git a/helix-term/src/ui/overlay.rs b/helix-term/src/ui/overlay.rs index ff184d4073fa..2dcbcdc920ed 100644 --- a/helix-term/src/ui/overlay.rs +++ b/helix-term/src/ui/overlay.rs @@ -15,6 +15,12 @@ pub struct Overlay { pub calc_child_size: Box Rect>, } +// TODO: For this to be sound, all of the various functions +// have to now be marked as send + sync + 'static. Annoying, +// and something I'll look into with steel. +unsafe impl Send for Overlay {} +unsafe impl Sync for Overlay {} + /// Surrounds the component with a margin of 5% on each side, and an additional 2 rows at the bottom pub fn overlaid(content: T) -> Overlay { Overlay { diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 6ba2fcb9e251..2000b691a6fb 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -39,6 +39,12 @@ pub struct Prompt { language: Option<(&'static str, Arc>)>, } +// TODO: For this to be sound, all of the various functions +// have to now be marked as send + sync + 'static. Annoying, +// and something I'll look into with steel. +unsafe impl Send for Prompt {} +unsafe impl Sync for Prompt {} + #[derive(Clone, Copy, PartialEq, Eq)] pub enum PromptEvent { /// The prompt input has been updated. diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 7437cbd074e5..49c818fb742a 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -424,6 +424,7 @@ where let path = rel_path .as_ref() .map(|p| p.to_string_lossy()) + .or_else(|| context.doc.name.as_ref().map(|x| x.into())) .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into()); format!(" {} ", path) }; @@ -483,6 +484,7 @@ where let path = rel_path .as_ref() .and_then(|p| p.file_name().map(|s| s.to_string_lossy())) + .or_else(|| context.doc.name.as_ref().map(|x| x.into())) .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into()); format!(" {} ", path) }; diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml index 96f008a01bf9..5247bd228117 100644 --- a/helix-tui/Cargo.toml +++ b/helix-tui/Cargo.toml @@ -13,6 +13,7 @@ homepage.workspace = true [features] default = ["crossterm"] +steel = ["dep:steel-core", "helix-view/steel", "helix-core/steel"] [dependencies] helix-view = { path = "../helix-view", features = ["term"] } @@ -26,3 +27,5 @@ termini = "1.0" serde = { version = "1", "optional" = true, features = ["derive"]} once_cell = "1.20" log = "~0.4" + +steel-core = { workspace = true, optional = true } diff --git a/helix-tui/src/extension.rs b/helix-tui/src/extension.rs new file mode 100644 index 000000000000..c046137cebcc --- /dev/null +++ b/helix-tui/src/extension.rs @@ -0,0 +1,20 @@ +#[cfg(feature = "steel")] +mod steel_implementations { + + use crate::{ + buffer::Buffer, + text::Text, + widgets::{Block, List, Paragraph, Table}, + }; + + use steel::{gc::unsafe_erased_pointers::CustomReference, rvals::Custom}; + + impl CustomReference for Buffer {} + impl Custom for Block<'static> {} + impl Custom for List<'static> {} + impl Custom for Paragraph<'static> {} + impl Custom for Table<'static> {} + impl Custom for Text<'static> {} + + steel::custom_reference!(Buffer); +} diff --git a/helix-tui/src/lib.rs b/helix-tui/src/lib.rs index 59327d7c348d..a4ffc186c713 100644 --- a/helix-tui/src/lib.rs +++ b/helix-tui/src/lib.rs @@ -130,6 +130,7 @@ pub mod backend; pub mod buffer; +pub mod extension; pub mod layout; pub mod symbols; pub mod terminal; diff --git a/helix-tui/src/widgets/list.rs b/helix-tui/src/widgets/list.rs index 4b0fc02f45bc..5e9add4b42b9 100644 --- a/helix-tui/src/widgets/list.rs +++ b/helix-tui/src/widgets/list.rs @@ -1,12 +1,12 @@ use crate::{ buffer::Buffer, - layout::{Corner, Rect}, - style::Style, + layout::Corner, text::Text, - widgets::{Block, StatefulWidget, Widget}, + widgets::{Block, Widget}, }; +use helix_core::unicode::width::UnicodeWidthStr; +use helix_view::graphics::{Rect, Style}; use std::iter::{self, Iterator}; -use unicode_width::UnicodeWidthStr; #[derive(Debug, Clone)] pub struct ListState { @@ -131,10 +131,8 @@ impl<'a> List<'a> { } } -impl<'a> StatefulWidget for List<'a> { - type State = ListState; - - fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { +impl<'a> List<'a> { + fn render_list(mut self, area: Rect, buf: &mut Buffer, state: &mut ListState) { buf.set_style(area, self.style); let list_area = match self.block.take() { Some(b) => { @@ -244,6 +242,6 @@ impl<'a> StatefulWidget for List<'a> { impl<'a> Widget for List<'a> { fn render(self, area: Rect, buf: &mut Buffer) { let mut state = ListState::default(); - StatefulWidget::render(self, area, buf, &mut state); + Self::render_list(self, area, buf, &mut state); } } diff --git a/helix-tui/src/widgets/mod.rs b/helix-tui/src/widgets/mod.rs index 3a0dfc5d8a0c..7145ec678cb7 100644 --- a/helix-tui/src/widgets/mod.rs +++ b/helix-tui/src/widgets/mod.rs @@ -10,13 +10,13 @@ //! - [`Paragraph`] mod block; -// mod list; +mod list; mod paragraph; mod reflow; mod table; pub use self::block::{Block, BorderType}; -// pub use self::list::{List, ListItem, ListState}; +pub use self::list::{List, ListItem, ListState}; pub use self::paragraph::{Paragraph, Wrap}; pub use self::table::{Cell, Row, Table, TableState}; diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index 725a77547cf9..1772ada851e6 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -11,8 +11,8 @@ repository.workspace = true homepage.workspace = true [features] -default = [] term = ["crossterm"] +steel = ["dep:steel-core", "helix-core/steel"] unicode-lines = [] [dependencies] @@ -49,6 +49,9 @@ serde_json = "1.0" toml = "0.8" log = "~0.4" +# plugin support +steel-core = { workspace = true, optional = true } + parking_lot = "0.12.3" thiserror.workspace = true diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 91ec27874853..dcf06566fef2 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -191,6 +191,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, } @@ -682,6 +684,7 @@ impl Document { config, version_control_head: None, focused_at: std::time::Instant::now(), + name: None, readonly: false, jump_labels: HashMap::new(), } @@ -1865,6 +1868,7 @@ impl Document { pub fn display_name(&self) -> Cow<'static, str> { self.relative_path() .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()) } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 26dea3a21e59..b63c1589a15b 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1081,6 +1081,17 @@ pub struct Editor { pub mouse_down_range: Option, pub cursor_cache: CursorCache, + + pub editor_clipping: ClippingConfiguration, + pub user_defined_themes: HashMap, +} + +#[derive(Default)] +pub struct ClippingConfiguration { + pub top: Option, + pub bottom: Option, + pub left: Option, + pub right: Option, } pub type Motion = Box; @@ -1099,6 +1110,7 @@ pub enum EditorEvent { pub enum ConfigEvent { Refresh, Update(Box), + Change, } enum ThemeAction { @@ -1198,6 +1210,8 @@ impl Editor { handlers, mouse_down_range: None, cursor_cache: CursorCache::default(), + editor_clipping: ClippingConfiguration::default(), + user_defined_themes: Default::default(), } } @@ -1559,6 +1573,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; @@ -1869,6 +1885,10 @@ impl Editor { // if leaving the view: mode should reset and the cursor should be // within view if prev_id != view_id { + log::info!("Changing focus: {:?}", view_id); + + // TODO: Consult map for modes to change given file type? + self.enter_normal_mode(); self.ensure_cursor_in_view(view_id); diff --git a/helix-view/src/extension.rs b/helix-view/src/extension.rs new file mode 100644 index 000000000000..32cba948be2e --- /dev/null +++ b/helix-view/src/extension.rs @@ -0,0 +1,75 @@ +use crate::DocumentId; + +pub fn document_id_to_usize(doc_id: &DocumentId) -> usize { + doc_id.0.into() +} + +#[cfg(feature = "steel")] +mod steel_implementations { + + use steel::{ + gc::unsafe_erased_pointers::CustomReference, + rvals::{as_underlying_type, Custom}, + }; + + use crate::{ + document::Mode, + editor::{ + Action, AutoSave, BufferLine, CursorShapeConfig, FilePickerConfig, GutterConfig, + IndentGuidesConfig, LineEndingConfig, LineNumber, LspConfig, SearchConfig, + SmartTabConfig, StatusLineConfig, TerminalConfig, WhitespaceConfig, + }, + graphics::{Color, Rect, Style, UnderlineStyle}, + input::Event, + Document, DocumentId, Editor, ViewId, + }; + + impl steel::gc::unsafe_erased_pointers::CustomReference for Editor {} + steel::custom_reference!(Editor); + + impl steel::rvals::Custom for Mode {} + impl steel::rvals::Custom for Event {} + impl Custom for Style { + fn fmt(&self) -> Option> { + Some(Ok(format!("{:?}", self))) + } + } + impl Custom for Color { + fn fmt(&self) -> Option> { + Some(Ok(format!("{:?}", self))) + } + } + impl Custom for UnderlineStyle {} + + impl CustomReference for Event {} + impl Custom for Rect { + fn equality_hint(&self, other: &dyn steel::rvals::CustomType) -> bool { + if let Some(other) = as_underlying_type::(other) { + self == other + } else { + false + } + } + } + impl Custom for crate::graphics::CursorKind {} + impl Custom for DocumentId {} + impl Custom for ViewId {} + impl CustomReference for Document {} + + impl Custom for Action {} + + impl Custom for FilePickerConfig {} + impl Custom for StatusLineConfig {} + impl Custom for SearchConfig {} + impl Custom for TerminalConfig {} + impl Custom for WhitespaceConfig {} + impl Custom for CursorShapeConfig {} + impl Custom for BufferLine {} + impl Custom for LineNumber {} + impl Custom for GutterConfig {} + impl Custom for LspConfig {} + impl Custom for IndentGuidesConfig {} + impl Custom for LineEndingConfig {} + impl Custom for SmartTabConfig {} + impl Custom for AutoSave {} +} diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index a26823b97c03..1c47a9a25813 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -292,6 +292,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 ca783fef54a2..e1351d683326 100644 --- a/helix-view/src/info.rs +++ b/helix-view/src/info.rs @@ -37,20 +37,32 @@ impl Info { .unwrap(); let mut text = String::new(); + let mut height = 0; + for (item, desc) in body { - let _ = writeln!( - text, - "{:width$} {}", - item.as_ref(), - desc.as_ref(), - width = item_width - ); + let mut line_iter = desc.as_ref().lines(); + + if let Some(first_line) = line_iter.next() { + let _ = writeln!( + text, + "{:width$} {}", + item.as_ref(), + first_line, + width = item_width + ); + height += 1; + } + + for line in line_iter { + let _ = writeln!(text, "{:width$} {}", "", line, width = item_width); + height += 1; + } } Self { title: title.to_string(), - width: text.lines().map(|l| l.width()).max().unwrap() as u16, - height: body.len() as u16, + width: text.lines().map(|l| l.width()).max().unwrap_or(body.len()) as u16, + height, text, } } diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index d54b49ef5400..ad6368ab3fa8 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 9dc326444b9b..1769d662f50f 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -9,6 +9,7 @@ use helix_core::hashmap; use helix_loader::merge_toml_values; use log::warn; use once_cell::sync::Lazy; +use rustix::path::Arg; use serde::{Deserialize, Deserializer}; use toml::{map::Map, Value}; @@ -307,10 +308,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. @@ -356,7 +374,7 @@ impl Theme { }) } - fn from_toml(value: Value) -> (Self, Vec) { + pub fn from_toml(value: Value) -> (Self, Vec) { if let Value::Table(table) = value { Theme::from_keys(table) } else { @@ -378,7 +396,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 be8bd4e5bcf5..1712a6da2c9b 100644 --- a/helix-view/src/tree.rs +++ b/helix-view/src/tree.rs @@ -669,6 +669,13 @@ impl Tree { pub fn area(&self) -> Rect { self.area } + + pub fn view_id_area(&self, id: ViewId) -> Option { + self.nodes.get(id).map(|node| match &node.content { + Content::View(v) => v.area, + Content::Container(c) => c.area, + }) + } } #[derive(Debug)] diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 7c7cb7f417c5..a65d0ac34d70 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.74.0" +channel = "1.80.0" components = ["rustfmt", "rust-src", "clippy"] diff --git a/steel b/steel new file mode 160000 index 000000000000..cf7a3df2c1cf --- /dev/null +++ b/steel @@ -0,0 +1 @@ +Subproject commit cf7a3df2c1cf0b0e1df53e127512f9fbde48476a 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 fcb462f252d5..930111f4d105 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,3 +1,4 @@ +mod codegen; mod docgen; mod helpers; mod path; @@ -9,12 +10,15 @@ use std::{env, error::Error}; type DynError = Box; pub mod tasks { + use crate::codegen::code_gen; use crate::docgen::{lang_features, typable_commands, write}; use crate::docgen::{LANG_SUPPORT_MD_OUTPUT, TYPABLE_COMMANDS_MD_OUTPUT}; use crate::querycheck::query_check; use crate::theme_check::theme_check; use crate::DynError; + use std::path::{Path, PathBuf}; + pub fn docgen() -> Result<(), DynError> { write(TYPABLE_COMMANDS_MD_OUTPUT, &typable_commands()?); write(LANG_SUPPORT_MD_OUTPUT, &lang_features()?); @@ -25,6 +29,73 @@ pub mod tasks { query_check() } + pub fn codegen() { + code_gen() + } + + pub fn install_steel() { + fn workspace_dir() -> PathBuf { + let output = std::process::Command::new(env!("CARGO")) + .arg("locate-project") + .arg("--workspace") + .arg("--message-format=plain") + .output() + .unwrap() + .stdout; + let cargo_path = Path::new(std::str::from_utf8(&output).unwrap().trim()); + cargo_path.parent().unwrap().to_path_buf() + } + + // Update the steel submodule + std::process::Command::new("git") + .args(["submodule", "init"]) + .spawn() + .unwrap() + .wait() + .unwrap(); + + std::process::Command::new("git") + .args(["submodule", "foreach", "git", "pull", "origin", "master"]) + .spawn() + .unwrap() + .wait() + .unwrap(); + + let mut workspace_dir = workspace_dir(); + + workspace_dir.push("steel"); + + std::process::Command::new("cargo") + .args(["xtask", "install"]) + .current_dir(workspace_dir) + .spawn() + .unwrap() + .wait() + .unwrap(); + + println!("=> Finished installing steel"); + + code_gen(); + + let helix_scm_path = helix_term::commands::helix_module_file(); + let steel_init_path = helix_term::commands::steel_init_file(); + + if !helix_scm_path.exists() { + std::fs::File::create(helix_scm_path).expect("Unable to create new helix.scm file!"); + } + + if !steel_init_path.exists() { + std::fs::File::create(steel_init_path).expect("Unable to create new init.scm file!"); + } + + std::process::Command::new("cargo") + .args(["install", "--path", "helix-term", "--locked", "--force"]) + .spawn() + .unwrap() + .wait() + .unwrap(); + } + pub fn themecheck() -> Result<(), DynError> { theme_check() } @@ -37,6 +108,8 @@ Usage: Run with `cargo xtask `, eg. `cargo xtask docgen`. Tasks: docgen: Generate files to be included in the mdbook output. query-check: Check that tree-sitter queries are valid. + code-gen: Generate files associated with steel + steel: Install steel " ); } @@ -49,6 +122,8 @@ fn main() -> Result<(), DynError> { Some(t) => match t.as_str() { "docgen" => tasks::docgen()?, "query-check" => tasks::querycheck()?, + "code-gen" => tasks::codegen(), + "steel" => tasks::install_steel(), "theme-check" => tasks::themecheck()?, invalid => return Err(format!("Invalid task name: {}", invalid).into()), },