From f193be17314709fcc426a96eebc8a9117d671939 Mon Sep 17 00:00:00 2001 From: xenia Date: Sun, 12 Nov 2023 00:21:50 +0100 Subject: [PATCH] First commit --- .envrc | 1 + .gitignore | 2 + Cargo.lock | 902 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 68 +++ README.original.asciidoc | 121 ++++++ UNLICENSE | 24 ++ build.rs | 51 +++ flake.lock | 195 +++++++++ flake.nix | 64 +++ rc/tree.kak | 79 ++++ src/config.rs | 70 +++ src/ffi.rs | 99 +++++ src/kakoune.rs | 89 ++++ src/log.rs | 19 + src/main.rs | 229 ++++++++++ src/tree.rs | 79 ++++ 16 files changed, 2092 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.original.asciidoc create mode 100644 UNLICENSE create mode 100644 build.rs create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 rc/tree.kak create mode 100644 src/config.rs create mode 100644 src/ffi.rs create mode 100644 src/kakoune.rs create mode 100644 src/log.rs create mode 100644 src/main.rs create mode 100644 src/tree.rs diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..599a890 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +.direnv diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..6df18a0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,902 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd66663db5a988098a89599d4857919b3acf7f61402e61365acfd3919857b9be" + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "deranged" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "itertools" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kak-tree" +version = "0.1.0" +dependencies = [ + "clap", + "itertools", + "serde", + "slog", + "slog-scope", + "sloggers", + "toml", + "tree-sitter", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "libflate" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9135df43b1f5d0e333385cb6e7897ecd1a43d7d11b91ac003f4d2c2d2401fdd" +dependencies = [ + "adler32", + "crc32fast", + "rle-decode-fast", + "take_mut", +] + +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.1", + "libc", + "redox_syscall", +] + +[[package]] +name = "log" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" +dependencies = [ + "log 0.4.20", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rle-decode-fast" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "serde" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "slog" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" + +[[package]] +name = "slog-async" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c8038f898a2c79507940990f05386455b3a317d8f18d4caea7cbc3d5096b84" +dependencies = [ + "crossbeam-channel", + "slog", + "take_mut", + "thread_local", +] + +[[package]] +name = "slog-kvfilter" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae939ed7d169eed9699f4f5cd440f046f5dc5dfc27c19e3cd311619594c175e0" +dependencies = [ + "regex", + "slog", +] + +[[package]] +name = "slog-scope" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f95a4b4c3274cd2869549da82b57ccc930859bdbf5bcea0424bc5f140b3c786" +dependencies = [ + "arc-swap", + "lazy_static", + "slog", +] + +[[package]] +name = "slog-stdlog" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c469573d1e3f36f9eee66cd132206caf47b50c94b1f6c6e7b4d8235e9ecf01" +dependencies = [ + "crossbeam", + "log 0.3.9", + "slog", + "slog-scope", +] + +[[package]] +name = "slog-term" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87d29185c55b7b258b4f120eab00f48557d4d9bc814f41713f449d35b0f8977c" +dependencies = [ + "atty", + "slog", + "term", + "thread_local", + "time", +] + +[[package]] +name = "sloggers" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31bef221d42166d6708aa1e9b0182324b37a0a7517ff590ec201dbfe1cfa46ef" +dependencies = [ + "chrono", + "libflate", + "regex", + "serde", + "serde_derive", + "slog", + "slog-async", + "slog-kvfilter", + "slog-scope", + "slog-stdlog", + "slog-term", + "trackable 0.2.24", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[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.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +dependencies = [ + "deranged", + "itoa", + "libc", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +dependencies = [ + "time-core", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "trackable" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98abb9e7300b9ac902cc04920945a874c1973e08c310627cc4458c04b70dd32" +dependencies = [ + "trackable 1.3.0", + "trackable_derive", +] + +[[package]] +name = "trackable" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15bd114abb99ef8cee977e517c8f37aee63f184f2d08e3e6ceca092373369ae" +dependencies = [ + "trackable_derive", +] + +[[package]] +name = "trackable_derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebeb235c5847e2f82cfe0f07eb971d1e5f6804b18dac2ae16349cc604380f82f" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tree-sitter" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "376e181cb69da67bad2d69806cf2500656fd68123c526a61e5cbbfc65110c5b0" +dependencies = [ + "cc", + "regex", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +dependencies = [ + "bumpalo", + "log 0.4.20", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3c12e58 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,68 @@ +[package] +name = "kak-tree" +version = "0.1.0" +authors = ["Ruslan Prokopchuk "] +edition = "2018" + +[dependencies] +clap = "2.33.0" +itertools = "0.8.2" +serde = { version = "1.0.104", features = ["derive"] } +slog = { version = "2.5.2", features = ["release_max_level_debug"] } +slog-scope = "4.3.0" +sloggers = "0.3.5" +toml = "0.5.6" +tree-sitter = "0.6.0" + +# [profile.release] +# lto = true + +[features] +default = ["rust"] +all = [ + "bash", + "c_sharp", + "c", + "clojure", + "cpp", + "css", + "elm", + "go", + "haskell", + "html", + "java", + "javascript", + "json", + "julia", + "ocaml", + "php", + "python", + "racket", + "ruby", + "rust", + "scala", + "typescript" +] +bash = [] +c_sharp = [] +c = [] +clojure = [] +cpp = [] +css = [] +elm = [] +go = [] +haskell = [] +html = [] +java = [] +javascript = [] +json = [] +julia = [] +ocaml = [] +php = [] +python = [] +racket = [] +ruby = [] +rust = [] +scala = [] +typescript = [] + diff --git a/README.original.asciidoc b/README.original.asciidoc new file mode 100644 index 0000000..c700a03 --- /dev/null +++ b/README.original.asciidoc @@ -0,0 +1,121 @@ += Structural selections for Kakoune + +kak-tree is a plugin for Kakoune which enables selection of syntax tree nodes. Parsing is performed with https://github.com/tree-sitter/tree-sitter[tree-sitter]. + +Status: proof of concept, interface and overall development direction could change drastically based on feedback. + +== Installation + +Replace `"rust javascript"` with a list of languages you need. Or use `all` to build all supported +languages. Note that `all` build takes a long time, and resulting binary is quite fat which could +have a negative impact on responsiveness. + +---- +git clone --recurse-submodules +cargo install --path . --force --features "rust javascript" +cp rc/tree.kak ~/.config/kak/autoload/ +---- + +Look at `Cargo.toml` for a full list of supported languages. + +It is possible to check programmaticaly if kak-tree was built with support for a given filetype: + +---- +kak-tree --do-you-understand rust +---- + +If language is supported then exit code is 0 otherwise it's non-zero (1 at the moment, but it is not +guaranteed in future). + +== Usage + +Tree-sitter parsers produce very detailed syntax tree, many elements of which are not interesting +for day-to-day selection purposes. kak-tree introduces the concept of a _visible_ node. Node is +_visible_ when: + +. Node is named in the tree-sitter grammar for the given language (as opposed to anonymous nodes, +http://tree-sitter.github.io/tree-sitter/using-parsers#named-vs-anonymous-nodes[more]). +. Either there is no white/blacklist for the given filetype or node kind is whitelisted or not +blacklisted. See <> for details about white/blacklisting. + +Most of the kak-tree commands operate on _visible_ nodes and skip not _visible_ ones. + +[cols=2*] +|=== + +| tree-select-parent-node [] +| Select the closest visible ancestor or ancestor of KIND when provided. + +| tree-select-next-node [] +| Select the closest visible next sibling or next sibling of KIND when provided. + +| tree-select-previous-node [] +| Select the closest visible previous sibling or previous sibling of KIND when provided. + +| tree-select-children [] +| Select all immediate visible children or all descendants matching KIND when provided. + +| tree-select-first-child [] +| Select the first immediate visible children or the first descendant matching KIND when provided. + +| tree-node-sexp +| Show info box with a syntax tree of the main selection parent. +|=== + +== Configuration + +kak-tree supports configuration via a configuration file. As for now there is no default path to +load the configuration file, and it must be given using CLI option `--config` or `-c` for short: + +---- +set global tree_cmd 'kak-tree -c /path/to/kak-tree.toml' +---- + +=== Filetype configuration + +Configuration for specific filetypes should be provided like this: + +---- +[filetype.rust] +blacklist = ["identifier", "scoped_identifier", "string_literal"] +whitelist = ["function_item"] +group.identifier = ["identifier", "scoped_identifier"] +group.fn = ["function_item"] + +[filetype.javascript] +group.fn = ["function", "arrow_function"] +---- + +Configuration under the `[filetype.default]` key will be used for all filetypes without +configuration. Specific filetype configuration _doesn't_ extend default configuration but rather +overwrites it. + +==== White/blacklisting + +If `whitelist` array is provided then kak-tree selection will skip nodes which kinds are not whitelisted. +If `blacklist` array is provided then kak-tree selection will skip nodes which kinds are blacklisted. + +NOTE: `whitelist` takes precedence over `blacklist`. In the Rust example above kak-tree would expand +selection up to the function definition, ignoring other node kinds. + +NOTE: `tree-node-sexp` command is useful for exploring node kinds which appear in the specific code. + +Whitelisting or blacklisting node kinds could be tedious as tree-sitter parsers define many of them, +but it also could be rewarding as you will be able to quickly modify selection in scopes which +matter for you with fewer keystrokes. + +==== Kind groups + +Groups of node kinds serve a two-fold purpose: + +. Groups allow matching functionally similar node kinds (i.e. `identifier` and `scoped_identifier` +in Rust) by a single query. + +. Groups allow matching functionally similar nodes across filetypes (i.e. `function_item` in Rust +and `function` in JavaScript) as tree-sitter parsers don't use uniform node kind names. + +NOTE: `whitelist` and `blacklist` options doesn't expand groups yet. + +== License + +For kak-tree see UNLICENSE file. For tree-sitter and its parsers look at their repositories. diff --git a/UNLICENSE b/UNLICENSE new file mode 100644 index 0000000..68a49da --- /dev/null +++ b/UNLICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..fae59e8 --- /dev/null +++ b/build.rs @@ -0,0 +1,51 @@ +fn main() { + println!("cargo:rustc-link-search=native=lib"); + for lang in &[ + #[cfg(feature = "bash")] + "bash", + #[cfg(feature = "c_sharp")] + "c-sharp", + #[cfg(feature = "c")] + "c", + #[cfg(feature = "clojure")] + "clojure", + #[cfg(feature = "cpp")] + "cpp", + #[cfg(feature = "css")] + "css", + #[cfg(feature = "elm")] + "elm", + #[cfg(feature = "go")] + "go", + #[cfg(feature = "haskell")] + "haskell", + #[cfg(feature = "html")] + "html", + #[cfg(feature = "java")] + "java", + #[cfg(feature = "javascript")] + "javascript", + #[cfg(feature = "json")] + "json", + #[cfg(feature = "julia")] + "julia", + #[cfg(feature = "ocaml")] + "ocaml", + #[cfg(feature = "php")] + "php", + #[cfg(feature = "python")] + "python", + #[cfg(feature = "racket")] + "racket", + #[cfg(feature = "ruby")] + "ruby", + #[cfg(feature = "rust")] + "rust", + #[cfg(feature = "scala")] + "scala", + #[cfg(feature = "typescript")] + "typescript", + ] { + println!("cargo:rustc-link-lib=static=tree-sitter-{lang}"); + } +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..a2a0392 --- /dev/null +++ b/flake.lock @@ -0,0 +1,195 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1681202837, + "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "cfacdce06f30d2b68473a46042957675eebb3401", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_3": { + "inputs": { + "systems": "systems_3" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1699155732, + "narHash": "sha256-Wg4RmOGUEO4YCF0fEc/qiQ/3+BGC/f0qywIE8xEkIpY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "15492ddc2974ba426ea7e17116ea7aa44fc96dcd", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1681358109, + "narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1697009197, + "narHash": "sha256-viVRhBTFT8fPJTb1N3brQIpFZnttmwo3JVKNuWRVc3s=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "01441e14af5e29c9d27ace398e6dd0b293e25a54", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay", + "tree-sitters": "tree-sitters" + } + }, + "rust-overlay": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1699669856, + "narHash": "sha256-OIb0WAoEMUA1EH70AwpWabdEpvYt/kJChBnb7XiXAJs=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "efd15e11c8954051a47679e7718b4c2a9b68ce27", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_3": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "tree-sitters": { + "inputs": { + "flake-utils": "flake-utils_3", + "nixpkgs": "nixpkgs_3" + }, + "locked": { + "lastModified": 1699742193, + "narHash": "sha256-1pc59vyEpFwtHYKsEGAwxL7/4wiTmLmzKO6j61wqOeo=", + "ref": "refs/heads/main", + "rev": "bb561d2e33d4d9f9d8171df2484ba6ae8e810b74", + "revCount": 3, + "type": "git", + "url": "https://githug.xyz/xenia/tree-sitters" + }, + "original": { + "type": "git", + "url": "https://githug.xyz/xenia/tree-sitters" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..481b5eb --- /dev/null +++ b/flake.nix @@ -0,0 +1,64 @@ +{ + description = "kak-tree, packaged for nix"; + + inputs = { + flake-utils.url = "github:numtide/flake-utils"; + rust-overlay.url = "github:oxalica/rust-overlay"; + tree-sitters.url = "git+https://githug.xyz/xenia/tree-sitters"; + }; + + outputs = { self, nixpkgs, flake-utils, rust-overlay, tree-sitters }: + flake-utils.lib.eachDefaultSystem (sys: + let pkgs = import nixpkgs { + system = sys; + overlays = [ (import rust-overlay) ]; + }; + rust = pkgs.rust-bin.stable.latest.default.override { + extensions = [ "rust-src" "rust-analyzer" ]; + }; + platform = pkgs.makeRustPlatform { + rustc = rust; + cargo = rust; + }; + sitters = tree-sitters.packages.${sys}; + sitterlist = [ + sitters.agda sitters.bash sitters.c sitters.nix sitters.python sitters.rust + ]; + + kak-tree-bin = platform.buildRustPackage { + name = "kak-tree-bin"; + src = ./.; + cargoLock = { lockFile = ./Cargo.lock; }; + buildFeatures = ["bash" "c" "python" "rust"]; # TODO: Add agda, nix + inherit sitterlist; + preBuild = '' + mkdir lib + for sitter in $sitterlist ; do + echo "Copying $sitter" + cp $sitter/lib/* lib + done + ''; + doCheck = false; + }; + + in rec { + packages.bin = kak-tree-bin; + packages.kak-tree = pkgs.stdenv.mkDerivation { + name = "kak-tree"; + src = ./rc ; + rtpPath = "share/kak/autoload/plugins"; + + patchPhase = '' + sed -i -e 's|tree_cmd "kak-tree"|tree_cmd "${kak-tree-bin}/bin/kak-tree"|' tree.kak + ''; + buildPhase = '' + mkdir -p "$out/$rtpPath/kak-tree" + cp * "$out/$rtpPath/kak-tree" + ''; + }; + devShells.default = pkgs.mkShell { + packages = [ rust ]; + }; + } + ); +} diff --git a/rc/tree.kak b/rc/tree.kak new file mode 100644 index 0000000..2c11a10 --- /dev/null +++ b/rc/tree.kak @@ -0,0 +1,79 @@ +# Path to the kak-tree executable. +# To load config: set-option global tree_cmd "kak-tree --config /path/to/kak-tree.toml" +# To enable debug logging: set-option global tree_cmd "kak-tree -vvv" +declare-option str tree_cmd "kak-tree" + +# Path to the log file. +declare-option str tree_log "/tmp/kak-tree.log" + +# Option to store draft of the current buffer before passing to shell. +declare-option -hidden str tree_draft + +define-command -hidden tree-command -params 1..2 -docstring %{ + tree-command [] + Send request to kak-tree and evaluate response. +} %{ + evaluate-commands -draft -no-hooks %{exec '%'; set buffer tree_draft %val{selection}} + evaluate-commands %sh{ + +tree_draft=$(printf '%s.' "${kak_opt_tree_draft}" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | sed "s/$(printf '\t')/\\\\t/g") + +tree_draft=${tree_draft%.} + +printf ' +filetype = "%s" +selections_desc = "%s" +content = """ +%s""" +[op] +type = "%s" +%s +' "${kak_opt_filetype}" "${kak_selections_desc}" "${tree_draft}" $1 "$2" | ${kak_opt_tree_cmd} 2>${kak_opt_tree_log} + } +} + + +define-command -hidden tree-command-with-optional-kind -params 1..2 -docstring %{ + tree-command-with-optional-kind [] + Send request which optionally takes node kind. +} %{ + tree-command %arg{1} %sh{ + if [ -n "$2" ]; then + printf 'kind = "%s"' "$2" + fi + } +} + +define-command tree-select-parent-node -params ..1 -docstring %{ + tree-select-parent-node [] + Select the closest visible ancestor or ancestor of KIND when provided. +} %{ tree-command-with-optional-kind SelectParentNode %arg{1} } + +define-command tree-select-next-node -params ..1 -docstring %{ + tree-select-next-node [] + Select the closest visible next sibling or next sibling of KIND when provided. +} %{ tree-command-with-optional-kind SelectNextNode %arg{1} } + +define-command tree-select-previous-node -params ..1 -docstring %{ + tree-select-previous-node [] + Select the closest visible previous sibling or previous sibling of KIND when provided. +} %{ tree-command-with-optional-kind SelectPreviousNode %arg{1} } + +define-command tree-select-children -params ..1 -docstring %{ + tree-select-children [] + Select all immediate visible children or all descendants matching KIND when provided. +} %{ tree-command-with-optional-kind SelectChildren %arg{1} } + +define-command tree-node-sexp -docstring %{ + tree-node-sexp + Show info box with a syntax tree of the main selection parent. +} %{ tree-command NodeSExp } + +define-command tree-select-first-child -params ..1 -docstring %{ + tree-select-first-child [] + Select the first immediate visible children or the first descendant matching KIND when provided. +} %{ + tree-command-with-optional-kind SelectChildren %arg{1} + execute-keys +} + diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..357a3ae --- /dev/null +++ b/src/config.rs @@ -0,0 +1,70 @@ +use serde::Deserialize; +use std::collections::HashMap; +use toml; +use tree_sitter::Node; + +#[derive(Deserialize)] +pub struct Config { + #[serde(default)] + filetype: HashMap, +} + +#[derive(Clone, Deserialize, Default)] +pub struct FiletypeConfig { + blacklist: Option>, + whitelist: Option>, + #[serde(default)] + group: HashMap>, +} + +impl Default for Config { + fn default() -> Self { + let mut config = Config { + filetype: HashMap::default(), + }; + config + .filetype + .insert("default".to_owned(), FiletypeConfig::default()); + config + } +} + +impl Config { + pub fn load>(path: P) -> Option { + let config = std::fs::read_to_string(path).ok()?; + let mut config: Config = toml::from_str(&config).ok()?; + if config.filetype.get("default").is_none() { + config + .filetype + .insert("default".to_owned(), FiletypeConfig::default()); + } + Some(config) + } + + pub fn get_filetype_config<'a>(&'a self, filetype: &str) -> &'a FiletypeConfig { + self.filetype + .get(filetype) + .or_else(|| self.filetype.get("default")) + .unwrap() + } +} + +impl FiletypeConfig { + pub fn is_node_visible(&self, node: Node) -> bool { + let kind = node.kind(); + match &self.whitelist { + Some(whitelist) => whitelist.iter().any(|x| x == kind), + None => match &self.blacklist { + Some(blacklist) => !blacklist.iter().any(|x| x == kind), + None => true, + }, + } + } + + pub fn resolve_alias<'a>(&'a self, kind: &str) -> Vec { + self.group + .get(kind) + .cloned() + .unwrap_or_else(|| vec![kind.to_string()]) + } +} diff --git a/src/ffi.rs b/src/ffi.rs new file mode 100644 index 0000000..c96641c --- /dev/null +++ b/src/ffi.rs @@ -0,0 +1,99 @@ +use tree_sitter::Language; + +extern "C" { + #[cfg(feature = "bash")] + fn tree_sitter_bash() -> Language; + #[cfg(feature = "c")] + fn tree_sitter_c() -> Language; + #[cfg(feature = "c_sharp")] + fn tree_sitter_c_sharp() -> Language; + #[cfg(feature = "clojure")] + fn tree_sitter_clojure() -> Language; + #[cfg(feature = "cpp")] + fn tree_sitter_cpp() -> Language; + #[cfg(feature = "css")] + fn tree_sitter_css() -> Language; + #[cfg(feature = "elm")] + fn tree_sitter_elm() -> Language; + #[cfg(feature = "go")] + fn tree_sitter_go() -> Language; + #[cfg(feature = "haskell")] + fn tree_sitter_haskell() -> Language; + #[cfg(feature = "html")] + fn tree_sitter_html() -> Language; + #[cfg(feature = "java")] + fn tree_sitter_java() -> Language; + #[cfg(feature = "javascript")] + fn tree_sitter_javascript() -> Language; + #[cfg(feature = "json")] + fn tree_sitter_json() -> Language; + #[cfg(feature = "julia")] + fn tree_sitter_julia() -> Language; + #[cfg(feature = "ocaml")] + fn tree_sitter_ocaml() -> Language; + #[cfg(feature = "php")] + fn tree_sitter_php() -> Language; + #[cfg(feature = "python")] + fn tree_sitter_python() -> Language; + #[cfg(feature = "racket")] + fn tree_sitter_racket() -> Language; + #[cfg(feature = "ruby")] + fn tree_sitter_ruby() -> Language; + #[cfg(feature = "rust")] + fn tree_sitter_rust() -> Language; + #[cfg(feature = "scala")] + fn tree_sitter_scala() -> Language; + #[cfg(feature = "typescript")] + fn tree_sitter_typescript() -> Language; +} + +pub fn filetype_to_language(filetype: &str) -> Option { + let sitter = match filetype { + #[cfg(feature = "bash")] + "sh" => tree_sitter_bash, + #[cfg(feature = "c")] + "c" => tree_sitter_c, + #[cfg(feature = "c_sharp")] + "c_sharp" => tree_sitter_c_sharp, + #[cfg(feature = "clojure")] + "clojure" => tree_sitter_clojure, + #[cfg(feature = "cpp")] + "cpp" => tree_sitter_cpp, + #[cfg(feature = "css")] + "css" => tree_sitter_css, + #[cfg(feature = "elm")] + "elm" => tree_sitter_elm, + #[cfg(feature = "go")] + "go" => tree_sitter_go, + #[cfg(feature = "haskell")] + "haskell" => tree_sitter_haskell, + #[cfg(feature = "html")] + "html" => tree_sitter_html, + #[cfg(feature = "java")] + "java" => tree_sitter_java, + #[cfg(feature = "javascript")] + "javascript" => tree_sitter_javascript, + #[cfg(feature = "json")] + "json" => tree_sitter_json, + #[cfg(feature = "julia")] + "julia" => tree_sitter_julia, + #[cfg(feature = "ocaml")] + "ocaml" => tree_sitter_ocaml, + #[cfg(feature = "php")] + "php" => tree_sitter_php, + #[cfg(feature = "python")] + "python" => tree_sitter_python, + #[cfg(feature = "racket")] + "racket" => tree_sitter_racket, + #[cfg(feature = "ruby")] + "ruby" => tree_sitter_ruby, + #[cfg(feature = "rust")] + "rust" => tree_sitter_rust, + #[cfg(feature = "scala")] + "scala" => tree_sitter_scala, + #[cfg(feature = "typescript")] + "typescript" => tree_sitter_typescript, + _ => return None, + }; + Some(unsafe { sitter() }) +} diff --git a/src/kakoune.rs b/src/kakoune.rs new file mode 100644 index 0000000..1bc881b --- /dev/null +++ b/src/kakoune.rs @@ -0,0 +1,89 @@ +use itertools::Itertools; +use tree_sitter::{Point, Range}; + +pub fn select_ranges(buffer: &[String], ranges: &[Range]) -> String { + if ranges.is_empty() { + "fail no selections remaining".into() + } else { + format!("select {}", ranges_to_selections_desc(&buffer, &ranges)) + } +} + +pub fn ranges_to_selections_desc(buffer: &[String], ranges: &[Range]) -> String { + ranges + .iter() + .map(|range| { + let mut end_row = range.end_point.row; + let mut end_column = range.end_point.column; + if end_column > 0 { + end_column -= 1; + } else { + end_row -= 1; + end_column = 1_000_000; + } + format!( + "{},{}", + point_to_kak_coords(buffer, range.start_point), + point_to_kak_coords(buffer, Point::new(end_row, end_column)) + ) + }) + .join(" ") +} + +pub fn selections_desc_to_ranges(buffer: &[String], selections_desc: &str) -> Vec { + selections_desc + .split_whitespace() + .map(|selection_desc| selection_desc_to_range(buffer, selection_desc)) + .collect() +} + +fn selection_desc_to_range(buffer: &[String], selection_desc: &str) -> Range { + let mut range = selection_desc.split(','); + let start = range.next().unwrap(); + let end = range.next().unwrap(); + let (start_byte, start_point) = kak_coords_to_byte_and_point(buffer, start); + let (end_byte, end_point) = kak_coords_to_byte_and_point(buffer, end); + let reverse = start_byte > end_byte; + if reverse { + Range { + start_byte: end_byte, + end_byte: start_byte, + start_point: end_point, + end_point: start_point, + } + } else { + Range { + start_byte, + end_byte, + start_point, + end_point, + } + } +} + +fn point_to_kak_coords(buffer: &[String], p: Point) -> String { + let offset = buffer[p.row] + .char_indices() + .enumerate() + .find_map(|(column, (offset, _))| { + if column == p.column { + Some(offset) + } else { + None + } + }) + .unwrap_or_else(|| buffer[p.row].len()); + format!("{}.{}", p.row + 1, offset + 1) +} + +fn kak_coords_to_byte_and_point(buffer: &[String], coords: &str) -> (usize, Point) { + let mut coords = coords.split('.'); + let row = coords.next().unwrap().parse::().unwrap() - 1; + let offset = coords.next().unwrap().parse::().unwrap() - 1; + let byte = buffer[..row].iter().fold(0, |offset, c| offset + c.len()) + offset; + let column = buffer[row] + .char_indices() + .position(|(i, _)| i == offset) + .unwrap(); + (byte, Point::new(row, column)) +} diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 0000000..cb87199 --- /dev/null +++ b/src/log.rs @@ -0,0 +1,19 @@ +use sloggers::terminal::{Destination, TerminalLoggerBuilder}; +use sloggers::types::Severity; +use sloggers::Build; + +pub fn init_global_logger(verbosity: u8) { + let level = match verbosity { + 0 => Severity::Error, + 1 => Severity::Warning, + 2 => Severity::Info, + 3 => Severity::Debug, + _ => Severity::Trace, + }; + + let mut builder = TerminalLoggerBuilder::new(); + builder.level(level); + builder.destination(Destination::Stderr); + let logger = builder.build().unwrap(); + let _guard = slog_scope::set_global_logger(logger); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6f61c37 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,229 @@ +use crate::config::{Config, FiletypeConfig}; +use clap::{crate_version, App, Arg}; +use serde::Deserialize; +use std::io::Read; +use toml; +use tree_sitter::{Node, Parser, Range}; + +mod config; +mod ffi; +mod kakoune; +mod log; +mod tree; + +#[derive(Deserialize)] +struct Request { + op: Op, + filetype: String, + selections_desc: String, + content: String, +} + +#[derive(Deserialize)] +#[serde(tag = "type")] +enum Op { + NodeSExp, + SelectChildren { kind: Option }, + SelectNextNode { kind: Option }, + SelectParentNode { kind: Option }, + SelectPreviousNode { kind: Option }, +} + +fn main() { + let matches = cli(); + + let verbosity = matches.occurrences_of("v") as u8; + log::init_global_logger(verbosity); + + if let Some(filetype) = matches.value_of("do-you-understand") { + let language = ffi::filetype_to_language(filetype); + std::process::exit(if language.is_some() { 0 } else { 1 }); + } + + let config = if let Some(config_path) = matches.value_of("config") { + Config::load(config_path).unwrap() + } else { + Config::default() + }; + + let mut request = String::new(); + std::io::stdin().read_to_string(&mut request).unwrap(); + let request: Request = toml::from_str(&request).unwrap(); + let response = handle_request(&config, &request); + println!("{}", response); +} + +fn cli() -> clap::ArgMatches<'static> { + App::new("kak-tree") + .version(crate_version!()) + .author("Ruslan Prokopchuk ") + .about("Structural selections for Kakoune") + .arg( + Arg::with_name("config") + .short("c") + .long("config") + .value_name("FILE") + .help("Read config from FILE") + .takes_value(true), + ) + .arg( + Arg::with_name("do-you-understand") + .long("do-you-understand") + .value_name("FILETYPE") + .help("Exit with 0 if FILETYPE is supported, non-zero otherwise") + .takes_value(true), + ) + .arg( + Arg::with_name("v") + .short("v") + .multiple(true) + .help("Sets the level of verbosity"), + ) + .get_matches() +} + +fn handle_request(config: &Config, request: &Request) -> String { + let mut parser = Parser::new(); + let language = ffi::filetype_to_language(&request.filetype).unwrap(); + parser.set_language(language).unwrap(); + let tree = parser.parse(&request.content, None).unwrap(); + let buffer = request + .content + .split('\n') + .map(|s| format!("{}\n", s)) + .collect::>(); + let ranges = kakoune::selections_desc_to_ranges(&buffer, &request.selections_desc); + let mut new_ranges = Vec::new(); + let filetype_config = config.get_filetype_config(&request.filetype); + match &request.op { + Op::SelectParentNode { kind } => { + let kinds = kind + .as_ref() + .and_then(|kind| Some(filetype_config.resolve_alias(kind))); + for range in &ranges { + let node = tree::shrink_to_range(tree.root_node(), range); + let node = find_parent_of_interest(filetype_config, node, &kinds); + new_ranges.push(node.range()); + } + kakoune::select_ranges(&buffer, &new_ranges) + } + Op::SelectNextNode { kind } => { + let kinds = kind + .as_ref() + .and_then(|kind| Some(filetype_config.resolve_alias(kind))); + 'outer_next: for range in &ranges { + let node = tree::shrink_to_range(tree.root_node(), range); + let mut cursor = traverse_up_to_node_which_matters(filetype_config, node); + while let Some(node) = cursor.next_named_sibling() { + if filetype_config.is_node_visible(node) && node_of_kinds(node, &kinds) { + new_ranges.push(node.range()); + continue 'outer_next; + } + cursor = node; + } + let node = find_parent_of_interest(filetype_config, node, &kinds); + new_ranges.push(node.range()); + } + kakoune::select_ranges(&buffer, &new_ranges) + } + Op::SelectPreviousNode { kind } => { + let kinds = kind + .as_ref() + .and_then(|kind| Some(filetype_config.resolve_alias(kind))); + 'outer_prev: for range in &ranges { + let node = tree::shrink_to_range(tree.root_node(), range); + let mut cursor = traverse_up_to_node_which_matters(filetype_config, node); + while let Some(node) = cursor.prev_named_sibling() { + if filetype_config.is_node_visible(node) && node_of_kinds(node, &kinds) { + new_ranges.push(node.range()); + continue 'outer_prev; + } + cursor = node; + } + let node = find_parent_of_interest(filetype_config, node, &kinds); + new_ranges.push(node.range()); + } + kakoune::select_ranges(&buffer, &new_ranges) + } + Op::SelectChildren { kind } => { + match kind { + Some(kind) => { + let kinds = filetype_config.resolve_alias(kind); + for range in &ranges { + for node in tree::nodes_in_range(tree.root_node(), range) { + select_nodes(&node, &kinds, &mut new_ranges); + } + } + } + None => { + for range in &ranges { + let node = tree::shrink_to_range(tree.root_node(), range); + for child in tree::named_children(&node) { + if filetype_config.is_node_visible(child) { + new_ranges.push(child.range()); + } + } + } + } + } + kakoune::select_ranges(&buffer, &new_ranges) + } + Op::NodeSExp => { + let node = tree::shrink_to_range(tree.root_node(), &ranges[0]); + format!("info '{}'", node.to_sexp()) + } + } +} + +fn select_nodes(node: &Node, kinds: &[String], new_ranges: &mut Vec) { + if kinds.iter().any(|kind| kind == node.kind()) { + new_ranges.push(node.range()); + } else { + for child in tree::named_children(&node) { + if kinds.iter().any(|kind| kind == child.kind()) { + new_ranges.push(child.range()); + } else { + select_nodes(&child, kinds, new_ranges); + } + } + } +} + +fn traverse_up_to_node_which_matters<'a>( + filetype_config: &FiletypeConfig, + current_node: Node<'a>, +) -> Node<'a> { + let mut opt_node = Some(current_node); + while let Some(node) = opt_node.filter(|&n| !(n.is_named() && filetype_config.is_node_visible(n))) { + opt_node = node.parent(); + } + opt_node.unwrap_or(current_node) +} + +fn find_parent_of_interest<'a>( + filetype_config: &FiletypeConfig, + current_node: Node<'a>, + kinds: &Option>, +) -> Node<'a> { + let parent = current_node.parent(); + match &kinds { + Some(kinds) => { + let mut cursor = parent; + while let Some(node) = cursor { + if kinds.iter().any(|kind| kind == node.kind()) { + return node; + } + cursor = node.parent(); + } + current_node + } + None => traverse_up_to_node_which_matters(filetype_config, parent.unwrap_or(current_node)), + } +} + +fn node_of_kinds(node: Node, kinds: &Option>) -> bool { + kinds + .as_ref() + .and_then(|kinds| Some(kinds.iter().any(|x| x == node.kind()))) + .unwrap_or(true) +} diff --git a/src/tree.rs b/src/tree.rs new file mode 100644 index 0000000..92a3e2d --- /dev/null +++ b/src/tree.rs @@ -0,0 +1,79 @@ +use tree_sitter::{Node, Range}; + +pub fn named_children<'a>(node: &'a Node) -> impl Iterator> { + (0..node.child_count()).map(move |i| node.child(i).unwrap()) +} + +pub fn shrink_to_range<'a>(root_node: Node<'a>, range: &Range) -> Node<'a> { + let mut node = root_node; + 'outer: loop { + let parent = node; + let mut cursor = parent.walk(); + for child in parent.children(&mut cursor) { + if child.range().start_byte <= range.start_byte + && range.end_byte <= child.range().end_byte + { + node = child; + continue 'outer; + } + } + return highest_node_of_same_range(node); + } +} + +pub fn nodes_in_range<'a>(root_node: Node<'a>, range: &Range) -> Vec> { + let mut nodes = Vec::new(); + let node = shrink_to_range(root_node, range); + if node.range().start_byte >= range.start_byte && range.end_byte >= node.range().end_byte { + nodes.push(node); + return nodes; + } + let mut cursor = node.walk(); + for child in node.children(&mut cursor) { + if child.range().start_byte <= range.start_byte && range.end_byte >= child.range().end_byte + { + nodes.extend(nodes_in_range( + child, + &Range { + start_byte: range.start_byte, + start_point: range.start_point, + end_byte: child.range().end_byte, + end_point: child.range().end_point, + }, + )); + } else if child.range().start_byte >= range.start_byte + && range.end_byte <= child.range().end_byte + { + nodes.extend(nodes_in_range( + child, + &Range { + start_byte: child.range().start_byte, + start_point: child.range().start_point, + end_byte: range.end_byte, + end_point: range.end_point, + }, + )); + } else if child.range().start_byte >= range.start_byte + && range.end_byte >= child.range().end_byte + { + nodes.push(child); + } + } + nodes +} + +fn highest_node_of_same_range<'a>(current_node: Node<'a>) -> Node<'a> { + let start_byte = current_node.start_byte(); + let end_byte = current_node.end_byte(); + let mut node = current_node; + while let Some(parent) = node.parent().and_then(|parent| { + if parent.start_byte() < start_byte || end_byte < parent.end_byte() { + None + } else { + Some(parent) + } + }) { + node = parent + } + node +}