diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2163ad4..7422053 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,70 +1,88 @@ name: CI on: push: { branches: ["0.x"] } pull_request: { branches: ["0.x"] } jobs: luacheck: name: Luacheck runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Install luarocks run: sudo apt update && sudo apt install -y luarocks - name: Install luacheck run: sudo luarocks install luacheck - name: Run luacheck run: luacheck . stylua: name: StyLua runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Run stylua - uses: JohnnyMorganz/stylua-action@v3.0.0 + uses: JohnnyMorganz/stylua-action@v4.0.0 with: token: ${{ secrets.GITHUB_TOKEN }} version: latest args: --check . cargo-format: name: Cargo Format runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up rust uses: dtolnay/rust-toolchain@stable with: components: rustfmt - name: Lint run: cargo fmt --check test: name: Build and test + strategy: + matrix: + nvim-version: ["v0.9.5", "stable", "nightly"] runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up rust uses: dtolnay/rust-toolchain@stable - name: Install dependencies - run: sudo apt update && sudo apt install -y luajit build-essential + run: sudo apt update && sudo apt install -y build-essential luarocks + + - name: Install busted + run: sudo luarocks install busted + + - name: Install neovim + run: | + test -d _neovim || { + mkdir -p _neovim + curl -sL "https://github.com/neovim/neovim/releases/download/${{ matrix.nvim-version }}/nvim-linux64.tar.gz" | tar xzf - --strip-components=1 -C "${PWD}/_neovim" + } - name: Build run: cargo build --release - - name: Test - run: find lua -name "*_test.lua" | xargs luajit scripts/test.lua + - name: Run tests + run: | + export PATH="${PWD}/_neovim/bin:${PATH}" + export VIM="${PWD}/_neovim/share/nvim/runtime" + + nvim --version + nvim -l ./scripts/busted.lua -o TAP ./lua/ivy/ 2> /dev/null diff --git a/.luacheckrc b/.luacheckrc index 337ac8e..2d46e75 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,18 +1,21 @@ -- Rerun tests only if their modification time changed. cache = true std = luajit codes = true self = false -- Global objects defined by the C code read_globals = { "vim", - "it", "after", "after_each", + "assert", "before", "before_each", + "describe", + "it", + "spy", } diff --git a/Cargo.lock b/Cargo.lock index 60d34ec..17ec5d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,753 +1,753 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] [[package]] name = "anes" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstyle" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" [[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 = "bstr" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" dependencies = [ "memchr", "serde", ] [[package]] name = "bumpalo" version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "ciborium" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0c137568cc60b904a7724001b35ce2630fd00d5d84805fbb608ab89509d788f" dependencies = [ "ciborium-io", "ciborium-ll", "serde", ] [[package]] name = "ciborium-io" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "346de753af073cc87b52b2083a506b38ac176a44cfb05497b622e27be899b369" [[package]] name = "ciborium-ll" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213030a2b5a4e0c0892b6652260cf6ccac84827b83a85a534e178e3906c4cf1b" dependencies = [ "ciborium-io", "half", ] [[package]] name = "clap" version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93aae7a4192245f70fe75dd9157fc7b4a5bf53e88d30bd4396f7d8f9284d5acc" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f423e341edefb78c9caba2d9c7f7687d0e72e89df3ce3394554754393ac3990" dependencies = [ "anstyle", "bitflags", "clap_lex", ] [[package]] name = "clap_lex" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" [[package]] name = "criterion" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" dependencies = [ "anes", "cast", "ciborium", "clap", "criterion-plot", "is-terminal", "itertools", "num-traits", "once_cell", "oorandom", "plotters", "rayon", "regex", "serde", "serde_derive", "serde_json", "tinytemplate", "walkdir", ] [[package]] name = "criterion-plot" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", "itertools", ] [[package]] name = "crossbeam-deque" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", "memoffset", "once_cell", "scopeguard", ] [[package]] name = "crossbeam-utils" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" dependencies = [ "cfg-if", "once_cell", ] [[package]] name = "either" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "errno" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", "windows-sys", ] [[package]] name = "errno-dragonfly" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" dependencies = [ "cc", "libc", ] [[package]] name = "fuzzy-matcher" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" dependencies = [ "thread_local", ] [[package]] name = "globset" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" dependencies = [ "aho-corasick", "bstr", "log", "regex-automata", "regex-syntax 0.8.2", ] [[package]] name = "half" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" [[package]] name = "hermit-abi" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" [[package]] name = "ignore" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" dependencies = [ "crossbeam-deque", "globset", "log", "memchr", "regex-automata", "same-file", "walkdir", "winapi-util", ] [[package]] name = "io-lifetimes" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi", "libc", "windows-sys", ] [[package]] name = "is-terminal" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" dependencies = [ "hermit-abi", "io-lifetimes", "rustix", "windows-sys", ] [[package]] name = "itertools" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8bf247779e67a9082a4790b45e71ac7cfd1321331a5c856a74a9faebdab78d0" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" [[package]] name = "ivy" version = "0.0.1" dependencies = [ "criterion", "fuzzy-matcher", "ignore", "rayon", ] [[package]] name = "js-sys" version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" dependencies = [ "wasm-bindgen", ] [[package]] name = "libc" version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" [[package]] name = "linux-raw-sys" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[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 = "memoffset" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" dependencies = [ "autocfg", ] [[package]] name = "num-traits" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" [[package]] name = "oorandom" version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "plotters" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97" dependencies = [ "num-traits", "plotters-backend", "plotters-svg", "wasm-bindgen", "web-sys", ] [[package]] name = "plotters-backend" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142" [[package]] name = "plotters-svg" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f" dependencies = [ "plotters-backend", ] [[package]] name = "proc-macro2" version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] [[package]] name = "rayon" -version = "1.8.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "regex" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" dependencies = [ "regex-syntax 0.6.27", ] [[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 0.8.2", ] [[package]] name = "regex-syntax" version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" [[package]] name = "regex-syntax" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "rustix" version = "0.37.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" dependencies = [ "bitflags", "errno", "io-lifetimes", "libc", "linux-raw-sys", "windows-sys", ] [[package]] name = "ryu" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" dependencies = [ "itoa", "ryu", "serde", ] [[package]] name = "syn" version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "thread_local" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" dependencies = [ "once_cell", ] [[package]] name = "tinytemplate" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ "serde", "serde_json", ] [[package]] name = "unicode-ident" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" [[package]] name = "walkdir" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasm-bindgen" version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" [[package]] name = "web-sys" version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" dependencies = [ "js-sys", "wasm-bindgen", ] [[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-util" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ "winapi", ] [[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-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 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.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] name = "windows_aarch64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_i686_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" diff --git a/Cargo.toml b/Cargo.toml index cf70aca..d36ee36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,31 +1,31 @@ [package] name = "ivy" version = "0.0.1" edition = "2021" [lib] name = "ivyrs" crate-type = ["cdylib", "rlib"] path = "rust/lib.rs" [dependencies] ignore = "0.4.22" fuzzy-matcher = "0.3.7" -rayon = "1.8.1" +rayon = "1.10.0" [dev-dependencies] criterion = "0.5.1" [profile.release] opt-level = 3 [profile.bench] debug = true [[bench]] name = "ivy_match" harness = false [[bench]] name = "ivy_files" harness = false diff --git a/README.md b/README.md index e9e8d1e..f048609 100644 --- a/README.md +++ b/README.md @@ -1,190 +1,248 @@
-ivy.vim +ivy.vim

An [ivy-mode](https://github.com/abo-abo/swiper#ivy) port to neovim. Ivy is a generic completion mechanism for ~~Emacs~~ Nvim
## Installation ### Manually ```sh git clone https://github.com/AdeAttwood/ivy.nvim ~/.config/nvim/pack/bundle/start/ivy.nvim ``` ### Plugin managers -TODO: Add docs in the plugin managers I don't use any +Using [lazy.nvim](https://github.com/folke/lazy.nvim) +```lua +{ + "AdeAttwood/ivy.nvim", + build = "cargo build --release", +}, +``` + +TODO: Add more plugin managers + +### Setup / Configuration + +Ivy can be configured with minimal config that will give you all the defaults +provided by Ivy. + +```lua +require('ivy').setup() +``` + +With Ivy you can configure your own backends. + +```lua +require('ivy').setup { + backends = { + -- A backend module that will be registered + "ivy.backends.buffers", + -- Using a table so you can configure a custom keymap overriding the + -- default one. + { "ivy.backends.files", { keymap = "" } } + }, +} +``` + +The `setup` function can only be called once, if its called a second time any +backends or config will not be used. Ivy does expose the `register_backend` +function, this can be used to load backends before or after the setup function +is called. + +```lua +require('ivy').register_backend("ivy.backends.files") +``` ### Compiling For the native searching, you will need to compile the shard library. You can do that by running the below command in the root of the plugin. ```sh cargo build --release ``` You will need to have the rust toolchain installed. You can find more about that [here](https://www.rust-lang.org/tools/install) If you get a linker error you may need to install `build-essential` to get `ld`. This is a common issue if you are running the [benchmarks](#benchmarks) in a VM ``` error: linker `cc` not found | = note: No such file or directory (os error 2) ``` To configure auto compiling you can use the [`post-merge`](./post-merge.sample) git hook to compile the library automatically when you update the package. This works well if you are managing your plugins via git. For an example installation you can run the following command. **NOTE:** This will overwrite any post-merge hooks you have already installed. ```sh cp ./post-merge.sample ./.git/hooks/post-merge ``` ## Features -### Commands +### Backends -A command can be run that will launch the completion UI +A backend is a module that will provide completion candidates for the UI to +show. It will also provide functionality when actions are taken. The Command +and Key Map are the default options provided by the backend, they can be +customized when you register it. -| Command | Key Map | Description | -| ------------------ | ----------- | ----------------------------------------------------------- | -| IvyFd | \p | Find files in your project with a custom rust file finder | -| IvyAg | \/ | Find content in files using the silver searcher | -| IvyBuffers | \b | Search though open buffers | -| IvyLines | | Search the lines in the current buffer | -| IvyWorkspaceSymbol | | Search for workspace symbols using the lsp workspace/symbol | +| Module | Command | Key Map | Description | +| ------------------------------------ | ------------------ | ----------- | ----------------------------------------------------------- | +| `ivy.backends.files` | IvyFd | \p | Find files in your project with a custom rust file finder | +| `ivy.backends.ag` | IvyAg | \/ | Find content in files using the silver searcher | +| `ivy.backends.rg` | IvyRg | \/ | Find content in files using ripgrep cli tool | +| `ivy.backends.buffers` | IvyBuffers | \b | Search though open buffers | +| `ivy.backends.lines` | IvyLines | | Search the lines in the current buffer | +| `ivy.backends.lsp-workspace-symbols` | IvyWorkspaceSymbol | | Search for workspace symbols using the lsp workspace/symbol | ### Actions Action can be run on selected candidates provide functionality -| Action | Description | -| -------------- | ------------------------------------------------------------------------------ | -| Complete | Run the completion function, usually this will be opening a file | -| Peek | Run the completion function on a selection, but don't close the results window | -| Vertical Split | Run the completion function in a new vertical split | -| Split | Run the completion function in a new split | +| Action | Key Map | Description | +| -------------- | ----------- | ------------------------------------------------------------------------------ | +| Complete | \ |Run the completion function, usually this will be opening a file | +| Vertical Split | \ |Run the completion function in a new vertical split | +| Split | \ |Run the completion function in a new split | +| Destroy | \ |Close the results window | +| Clear | \ |Clear the results window | +| Delete word | \ |Delete the word under the cursor | +| Next | \ |Move to the next candidate | +| Previous | \ |Move to the previous candidate | +| Next Checkpoint| \ |Move to the next candidate and keep Ivy open and focussed | +| Previous Checkpoint| \|Move to the previous candidate and keep Ivy open and focussed | + +Add your own keymaps for an action by adding a `ftplugin/ivy.lua` file in your config. +Just add a simple keymap like this: + +```lua +vim.api.nvim_set_keymap( "n", "", "lua vim.ivy.destroy()", { noremap = true, silent = true, nowait = true }) +``` + ## API ### ivy.run The `ivy.run` function is the core function in the plugin, it will launch the completion window and display the items from your items function. When the users accept one of the candidates with an [action](#actions), it will call the callback function to in most cases open the item in the desired location. ```lua ---@param name string ---@param items fun(input: string): { content: string }[] | string ---@param callback fun(result: string, action: string) vim.ivy.run = function(name, items, callback) end ``` #### Name `string` The name is the display name for the command and will be the name of the buffer in the completion window #### Items `fun(input: string): { content: string }[] | string` The items function is a function that will return the candidates to display in the completion window. This can return a string where each line will be a completion item. Or an array of tables where the `content` will be the completion item. #### Callback `fun(result: string, action: string)` The function that will run when the user selects a completion item. Generally this will open the item in the desired location. For example, in the file finder with will open the file in a new buffer. If the user selects the vertical split action it will open the buffer in a new `vsplit` #### Example ```lua vim.ivy.run( -- The name given to the results window and displayed to the user "Title", -- Call back function to get all the candidates that will be displayed in -- the results window, The `input` will be passed in, so you can filter -- your results with the value from the prompt function(input) return { { content = "One" }, { content = "Two" }, { content = "Three" }, } end, - -- Action callback that will be called on the completion or peek actions. + -- Action callback that will be called on the completion or checkpoint actions. -- The currently selected item is passed in as the result. function(result) vim.cmd("edit " .. result) end ) ``` ## Benchmarks Benchmarks are of various tasks that ivy will do. The purpose of the benchmarks are to give us a baseline on where to start when trying to optimize performance in the matching and sorting, not to put ivy against other tools. When starting to optimize, you will probably need to get a baseline on your hardware. There are fixtures provided that will create the directory structure of the [kubernetes](https://github.com/kubernetes/kubernetes) source code, from somewhere around commit sha 985c9202ccd250a5fe22c01faf0d8f83d804b9f3. This will create a directory tree of 23511 files a relative large source tree to get a good idea of performance. To create the source tree under `/tmp/ivy-trees/kubernetes` run the following command. This will need to be run for the benchmarks to run. ```bash # Create the source trees bash ./scripts/fixtures.bash # Run the benchmark script luajit ./scripts/benchmark.lua ``` Current benchmark status running on a `e2-standard-2` 2 vCPU + 8 GB memory VM running on GCP. IvyRs (Lua) | Name | Total | Average | Min | Max | | ---------------------------- | ------------- | ------------- | ------------- | ------------- | | ivy_match(file.lua) 1000000x | 04.153531 (s) | 00.000004 (s) | 00.000003 (s) | 00.002429 (s) | | ivy_files(kubernetes) 100x | 03.526795 (s) | 00.035268 (s) | 00.021557 (s) | 00.037127 (s) | IvyRs (Criterion) | Name | Min | Mean | Max | | --------------------- | --------- | --------- | --------- | | ivy_files(kubernetes) | 19.727 ms | 19.784 ms | 19.842 ms | | ivy_match(file.lua) | 2.6772 µs | 2.6822 µs | 2.6873 µs | CPP | Name | Total | Average | Min | Max | | ---------------------------- | ------------- | ------------- | ------------- | ------------- | | ivy_match(file.lua) 1000000x | 01.855197 (s) | 00.000002 (s) | 00.000001 (s) | 00.000177 (s) | | ivy_files(kubernetes) 100x | 14.696396 (s) | 00.146964 (s) | 00.056604 (s) | 00.168478 (s) | ## Other stuff you might like - [ivy-mode](https://github.com/abo-abo/swiper#ivy) - An emacs package that was the inspiration for this nvim plugin - [Command-T](https://github.com/wincent/command-t) - Vim plugin I used before I started this one - [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) - Another competition plugin, lots of people are using diff --git a/lua/ivy/config.lua b/lua/ivy/config.lua new file mode 100644 index 0000000..8ff1e6a --- /dev/null +++ b/lua/ivy/config.lua @@ -0,0 +1,33 @@ +local config_mt = {} +config_mt.__index = config_mt + +function config_mt:get_in(config, key_table) + local current_value = config + for _, key in ipairs(key_table) do + if current_value == nil then + return nil + end + + current_value = current_value[key] + end + + return current_value +end + +function config_mt:get(key_table) + return self:get_in(self.user_config, key_table) or self:get_in(self.default_config, key_table) +end + +local config = { user_config = {} } + +config.default_config = { + backends = { + "ivy.backends.buffers", + "ivy.backends.files", + "ivy.backends.lines", + "ivy.backends.rg", + "ivy.backends.lsp-workspace-symbols", + }, +} + +return setmetatable(config, config_mt) diff --git a/lua/ivy/config_spec.lua b/lua/ivy/config_spec.lua new file mode 100644 index 0000000..7ef8cd8 --- /dev/null +++ b/lua/ivy/config_spec.lua @@ -0,0 +1,27 @@ +local config = require "ivy.config" + +describe("config", function() + before_each(function() + config.user_config = {} + end) + + it("gets the first item when there is only default values", function() + local first_backend = config:get { "backends", 1 } + assert.is_equal("ivy.backends.buffers", first_backend) + end) + + it("returns nil if we access a key that is not a valid config item", function() + assert.is_nil(config:get { "not", "a", "thing" }) + end) + + it("returns the users overridden config value", function() + config.user_config = { backends = { "ivy.my.backend" } } + local first_backend = config:get { "backends", 1 } + assert.is_equal("ivy.my.backend", first_backend) + end) + + it("returns a nested value", function() + config.user_config = { some = { nested = "value" } } + assert.is_equal(config:get { "some", "nested" }, "value") + end) +end) diff --git a/lua/ivy/controller_spec.lua b/lua/ivy/controller_spec.lua new file mode 100644 index 0000000..6556cee --- /dev/null +++ b/lua/ivy/controller_spec.lua @@ -0,0 +1,57 @@ +local window = require "ivy.window" +local controller = require "ivy.controller" + +describe("controller", function() + before_each(function() + vim.cmd "highlight IvyMatch cterm=bold gui=bold" + window.initialize() + end) + + after_each(function() + controller.destroy() + end) + + it("will run the completion", function() + controller.run("Testing", function() + return { { content = "Some content" } } + end, function() + return {} + end) + + -- Run all the scheduled tasks + vim.wait(0) + + local lines = vim.api.nvim_buf_get_lines(0, 0, -1, true) + assert.is_equal(#lines, 1) + assert.is_equal(lines[1], "Some content") + end) + + it("will not try and highlight the buffer if there is nothing to highlight", function() + spy.on(vim, "cmd") + + controller.items = function() + return { { content = "Hello" } } + end + + controller.update "" + + vim.wait(0) + + assert.spy(vim.cmd).was_called_with "syntax clear IvyMatch" + assert.spy(vim.cmd).was_not_called_with "syntax match IvyMatch '[H]'" + end) + + it("will escape a - when passing it to be highlighted", function() + spy.on(vim, "cmd") + + controller.items = function() + return { { content = "Hello" } } + end + + controller.update "some-file" + + vim.wait(0) + + assert.spy(vim.cmd).was_called_with "syntax match IvyMatch '[some\\-file]'" + end) +end) diff --git a/lua/ivy/controller_test.lua b/lua/ivy/controller_test.lua deleted file mode 100644 index 9865142..0000000 --- a/lua/ivy/controller_test.lua +++ /dev/null @@ -1,51 +0,0 @@ -local vim_mock = require "ivy.vim_mock" -local window = require "ivy.window" -local controller = require "ivy.controller" - --- The number of the mock buffer where all the test completions gets put -local buffer_number = 10 - -before_each(function() - vim_mock.reset() - window.initialize() -end) - -after_each(function() - controller.destroy() -end) - -it("will run", function(t) - controller.run("Testing", function() - return { { content = "Some content" } } - end, function() - return {} - end) - - local lines = vim_mock.get_lines() - local completion_lines = lines[buffer_number] - - t.assert_equal(#completion_lines, 1) - t.assert_equal(completion_lines[1], "Some content") -end) - -it("will not try and highlight the buffer if there is nothing to highlight", function(t) - controller.items = function() - return { { content = "Hello" } } - end - - controller.update "" - local commands = vim_mock.get_commands() - t.assert_equal(#commands, 1) -end) - -it("will escape a - when passing it to be highlighted", function(t) - controller.items = function() - return { { content = "Hello" } } - end - - controller.update "some-file" - local commands = vim_mock.get_commands() - local syntax_command = commands[2] - - t.assert_equal("syntax match IvyMatch '[some\\-file]'", syntax_command) -end) diff --git a/lua/ivy/init.lua b/lua/ivy/init.lua new file mode 100644 index 0000000..58672ff --- /dev/null +++ b/lua/ivy/init.lua @@ -0,0 +1,32 @@ +local controller = require "ivy.controller" +local config = require "ivy.config" +local register_backend = require "ivy.register_backend" + +local ivy = {} +ivy.run = controller.run +ivy.register_backend = register_backend + +-- Private variable to check if ivy has been setup, this is to prevent multiple +-- setups of ivy. This is only exposed for testing purposes. +---@private +ivy.has_setup = false + +---@class IvySetupOptions +---@field backends (IvyBackend | { ["1"]: string, ["2"]: IvyBackendOptions} | string)[] + +---@param user_config IvySetupOptions +function ivy.setup(user_config) + if ivy.has_setup then + return + end + + config.user_config = user_config or {} + + for _, backend in ipairs(config:get { "backends" } or {}) do + register_backend(backend) + end + + ivy.has_setup = true +end + +return ivy diff --git a/lua/ivy/init_spec.lua b/lua/ivy/init_spec.lua new file mode 100644 index 0000000..f8ea91b --- /dev/null +++ b/lua/ivy/init_spec.lua @@ -0,0 +1,30 @@ +local ivy = require "ivy" +local config = require "ivy.config" + +describe("ivy.setup", function() + before_each(function() + ivy.has_setup = false + config.user_config = {} + end) + + it("sets the users config options", function() + ivy.setup { backends = { "ivy.backends.files" } } + assert.is_equal("ivy.backends.files", config:get { "backends", 1 }) + end) + + it("will not reconfigure if its called twice", function() + ivy.setup { backends = { "ivy.backends.files" } } + ivy.setup { backends = { "some.backend" } } + assert.is_equal("ivy.backends.files", config:get { "backends", 1 }) + end) + + it("does not crash if you don't pass in any params to the setup function", function() + ivy.setup() + assert.is_equal("ivy.backends.buffers", config:get { "backends", 1 }) + end) + + it("will fallback if the key is not set at all in the users config", function() + ivy.setup { some_key = "some_value" } + assert.is_equal("ivy.backends.buffers", config:get { "backends", 1 }) + end) +end) diff --git a/lua/ivy/libivy_spec.lua b/lua/ivy/libivy_spec.lua new file mode 100644 index 0000000..8faf503 --- /dev/null +++ b/lua/ivy/libivy_spec.lua @@ -0,0 +1,36 @@ +require "busted.runner"() + +local libivy = require "ivy.libivy" + +describe("libivy", function() + it("should run a simple match", function() + local score = libivy.ivy_match("term", "I am a serch term") + + assert.is_true(score > 0) + end) + + it("should find a dot file", function() + local current_dir = libivy.ivy_cwd() + local results = libivy.ivy_files(".github/workflows/ci.yml", current_dir) + + assert.is_equal(2, results.length, "Incorrect number of results found") + assert.is_equal(".github/workflows/ci.yml", results[2].content, "Invalid matches") + end) + + it("will allow you to access the length via the metatable", function() + local current_dir = libivy.ivy_cwd() + local results = libivy.ivy_files(".github/workflows/ci.yml", current_dir) + + local mt = getmetatable(results) + + assert.is_equal(results.length, mt.__len(results), "The `length` property does not match the __len metamethod") + end) + + it("will create an iterator", function() + local iter = libivy.ivy_files(".github/workflows/ci.yml", libivy.ivy_cwd()) + local mt = getmetatable(iter) + + assert.is_equal(type(mt["__index"]), "function") + assert.is_equal(type(mt["__len"]), "function") + end) +end) diff --git a/lua/ivy/libivy_test.lua b/lua/ivy/libivy_test.lua deleted file mode 100644 index fe18455..0000000 --- a/lua/ivy/libivy_test.lua +++ /dev/null @@ -1,46 +0,0 @@ -local libivy = require "ivy.libivy" - -it("should run a simple match", function(t) - local score = libivy.ivy_match("term", "I am a serch term") - - if score <= 0 then - t.error("Score should not be less than 0 found " .. score) - end -end) - -it("should find a dot file", function(t) - local current_dir = libivy.ivy_cwd() - local results = libivy.ivy_files(".github/workflows/ci.yml", current_dir) - - if results.length ~= 2 then - t.error("Incorrect number of results found " .. results.length) - end - - if results[2].content ~= ".github/workflows/ci.yml" then - t.error("Invalid matches: " .. results[2].content) - end -end) - -it("will allow you to access the length via the metatable", function(t) - local current_dir = libivy.ivy_cwd() - local results = libivy.ivy_files(".github/workflows/ci.yml", current_dir) - - local mt = getmetatable(results) - - if results.length ~= mt.__len(results) then - t.error "The `length` property does not match the __len metamethod" - end -end) - -it("will create an iterator", function(t) - local iter = libivy.ivy_files(".github/workflows/ci.yml", libivy.ivy_cwd()) - local mt = getmetatable(iter) - - if type(mt["__index"]) ~= "function" then - t.error "The iterator does not have an __index metamethod" - end - - if type(mt["__len"]) ~= "function" then - t.error "The iterator does not have an __len metamethod" - end -end) diff --git a/lua/ivy/matcher_spec.lua b/lua/ivy/matcher_spec.lua new file mode 100644 index 0000000..7128a2c --- /dev/null +++ b/lua/ivy/matcher_spec.lua @@ -0,0 +1,29 @@ +local libivy = require "ivy.libivy" + +-- Helper function to test a that string `one` has a higher match score than +-- string `two`. If string `one` has a lower score than string `two` a string +-- will be returned that can be used in body of an error. If not then `nil` is +-- returned and all is good. +local match_test = function(term, one, two) + local score_one = libivy.ivy_match(term, one) + local score_two = libivy.ivy_match(term, two) + + assert.is_true( + score_one > score_two, + ("The score of %s (%d) ranked higher than %s (%d)"):format(one, score_one, two, score_two) + ) +end + +describe("ivy matcher", function() + it("should match path separator", function() + match_test("file", "some/file.lua", "somefile.lua") + end) + + -- it("should match pattern with spaces", function() + -- match_test("so fi", "some/file.lua", "somefile.lua") + -- end) + + it("should match the start of a string", function() + match_test("file", "file.lua", "somefile.lua") + end) +end) diff --git a/lua/ivy/matcher_test.lua b/lua/ivy/matcher_test.lua deleted file mode 100644 index f80f565..0000000 --- a/lua/ivy/matcher_test.lua +++ /dev/null @@ -1,37 +0,0 @@ -local libivy = require "ivy.libivy" - --- Helper function to test a that string `one` has a higher match score than --- string `two`. If string `one` has a lower score than string `two` a string --- will be returned that can be used in body of an error. If not then `nil` is --- returned and all is good. -local match_test = function(term, one, two) - local score_one = libivy.ivy_match(term, one) - local score_two = libivy.ivy_match(term, two) - - if score_one < score_two then - return one .. " should be ranked higher than " .. two - end - - return nil -end - -it("sould match path separator", function(t) - local result = match_test("file", "some/file.lua", "somefile.lua") - if result then - t.error(result) - end -end) - -it("sould match pattern with spaces", function(t) - local result = match_test("so fi", "some/file.lua", "somefile.lua") - if result then - t.error(result) - end -end) - -it("sould match the start of a string", function(t) - local result = match_test("file", "file.lua", "somefile.lua") - if result then - t.error(result) - end -end) diff --git a/lua/ivy/prompt_spec.lua b/lua/ivy/prompt_spec.lua new file mode 100644 index 0000000..452a091 --- /dev/null +++ b/lua/ivy/prompt_spec.lua @@ -0,0 +1,91 @@ +local prompt = require "ivy.prompt" + +-- Input a list of strings into the prompt +local input = function(input_table) + for index = 1, #input_table do + prompt.input(input_table[index]) + end +end + +describe("prompt", function() + before_each(function() + prompt.destroy() + end) + + it("starts with empty text", function() + assert.is_same(prompt.text(), "") + end) + + it("can input some text", function() + input { "A", "d", "e" } + assert.is_same(prompt.text(), "Ade") + end) + + it("can delete a char", function() + input { "A", "d", "e", "BACKSPACE" } + assert.is_same(prompt.text(), "Ad") + end) + + it("will reset the text", function() + input { "A", "d", "e" } + prompt.set "New" + assert.is_same(prompt.text(), "New") + end) + + it("can move around the a word", function() + input { "P", "r", "o", "p", "t", "LEFT", "LEFT", "LEFT", "RIGHT", "m" } + assert.is_same(prompt.text(), "Prompt") + end) + + it("can delete a word", function() + prompt.set "Ade Attwood" + input { "DELETE_WORD" } + + assert.is_same(prompt.text(), "Ade ") + end) + + it("can delete a word in the middle and leave the cursor at that word", function() + prompt.set "Ade middle A" + input { "LEFT", "LEFT", "DELETE_WORD", "a" } + + assert.is_same(prompt.text(), "Ade a A") + end) + + it("will delete the space and the word if the last word is single space", function() + prompt.set "some.thing " + input { "DELETE_WORD" } + + assert.is_same(prompt.text(), "some.") + end) + + it("will only delete one word from path", function() + prompt.set "some/nested/path" + input { "DELETE_WORD" } + + assert.is_same(prompt.text(), "some/nested/") + end) + + it("will delete tailing space", function() + prompt.set "word " + input { "DELETE_WORD" } + + assert.is_same(prompt.text(), "") + end) + + it("will leave a random space", function() + prompt.set "some word " + input { "DELETE_WORD" } + + assert.is_same(prompt.text(), "some ") + end) + + local special_characters = { ".", "/", "^" } + for _, char in ipairs(special_characters) do + it(string.format("will stop at a %s", char), function() + prompt.set(string.format("key%sValue", char)) + input { "DELETE_WORD" } + + assert.is_same(prompt.text(), string.format("key%s", char)) + end) + end +end) diff --git a/lua/ivy/prompt_test.lua b/lua/ivy/prompt_test.lua deleted file mode 100644 index ee834b6..0000000 --- a/lua/ivy/prompt_test.lua +++ /dev/null @@ -1,94 +0,0 @@ -local prompt = require "ivy.prompt" -local vim_mock = require "ivy.vim_mock" - -before_each(function() - vim_mock.reset() - prompt.destroy() -end) - --- Input a list of strings into the prompt -local input = function(input_table) - for index = 1, #input_table do - prompt.input(input_table[index]) - end -end - --- Asserts the prompt contains the correct value -local assert_prompt = function(t, expected) - local text = prompt.text() - if text ~= expected then - t.error("The prompt text should be '" .. expected .. "' found '" .. text .. "'") - end -end - -it("starts with empty text", function(t) - if prompt.text() ~= "" then - t.error "The prompt should start with empty text" - end -end) - -it("can input some text", function(t) - input { "A", "d", "e" } - assert_prompt(t, "Ade") -end) - -it("can delete a char", function(t) - input { "A", "d", "e", "BACKSPACE" } - assert_prompt(t, "Ad") -end) - -it("will reset the text", function(t) - input { "A", "d", "e" } - prompt.set "New" - assert_prompt(t, "New") -end) - -it("can move around the a word", function(t) - input { "P", "r", "o", "p", "t", "LEFT", "LEFT", "LEFT", "RIGHT", "m" } - assert_prompt(t, "Prompt") -end) - -it("can delete a word", function(t) - prompt.set "Ade Attwood" - input { "DELETE_WORD" } - assert_prompt(t, "Ade ") -end) - -it("can delete a word in the middle", function(t) - prompt.set "Ade middle A" - input { "LEFT", "LEFT", "DELETE_WORD" } - assert_prompt(t, "Ade A") -end) - -it("will delete the space and the word if the last word is single space", function(t) - prompt.set "some.thing " - input { "DELETE_WORD" } - assert_prompt(t, "some.") -end) - -it("will only delete one word from path", function(t) - prompt.set "some/nested/path" - input { "DELETE_WORD" } - assert_prompt(t, "some/nested/") -end) - -it("will delete tailing space", function(t) - prompt.set "word " - input { "DELETE_WORD" } - assert_prompt(t, "") -end) - -it("will leave a random space", function(t) - prompt.set "some word " - input { "DELETE_WORD" } - assert_prompt(t, "some ") -end) - -local special_characters = { ".", "/", "^" } -for _, char in ipairs(special_characters) do - it(string.format("will stop at a %s", char), function(t) - prompt.set(string.format("key%sValue", char)) - input { "DELETE_WORD" } - assert_prompt(t, string.format("key%s", char)) - end) -end diff --git a/lua/ivy/register_backend.lua b/lua/ivy/register_backend.lua new file mode 100644 index 0000000..f314588 --- /dev/null +++ b/lua/ivy/register_backend.lua @@ -0,0 +1,54 @@ +---@class IvyBackend +---@field command string The command this backend will have +---@field items fun(input: string): { content: string }[] | string The callback function to get the items to select from +---@field callback fun(result: string, action: string) The callback function to run when a item is selected +---@field description string? The description of the backend, this will be used in the keymaps +---@field name string? The name of the backend, this will fallback to the command if its not set +---@field keymap string? The keymap to trigger this backend + +---@class IvyBackendOptions +---@field command string The command this backend will have +---@field keymap string? The keymap to trigger this backend + +---Register a new backend +--- +---This will create all the commands and set all the keymaps for the backend +---@param backend IvyBackend +local register_backend_class = function(backend) + local user_command_options = { bang = true } + if backend.description ~= nil then + user_command_options.desc = backend.description + end + + local name = backend.name or backend.command + vim.api.nvim_create_user_command(backend.command, function() + vim.ivy.run(name, backend.items, backend.callback) + end, user_command_options) + + if backend.keymap ~= nil then + vim.api.nvim_set_keymap("n", backend.keymap, "" .. backend.command .. "", { nowait = true, silent = true }) + end +end + +---@param backend IvyBackend | { ["1"]: string, ["2"]: IvyBackendOptions} | string The backend or backend module +---@param options IvyBackendOptions? The options for the backend, that will be merged with the backend +local register_backend = function(backend, options) + if type(backend[1]) == "string" then + options = backend[2] + backend = require(backend[1]) + end + + if type(backend) == "string" then + backend = require(backend) + end + + if options then + for key, value in pairs(options) do + backend[key] = value + end + end + + register_backend_class(backend) +end + +return register_backend diff --git a/lua/ivy/register_backend_spec.lua b/lua/ivy/register_backend_spec.lua new file mode 100644 index 0000000..ab51e4e --- /dev/null +++ b/lua/ivy/register_backend_spec.lua @@ -0,0 +1,71 @@ +local register_backend = require "ivy.register_backend" + +local function get_command(name) + local command_iter = vim.api.nvim_get_commands {} + + for _, cmd in pairs(command_iter) do + if cmd.name == name then + return cmd + end + end + + return nil +end + +local function get_keymap(mode, rhs) + local keymap_iter = vim.api.nvim_get_keymap(mode) + for _, keymap in pairs(keymap_iter) do + if keymap.rhs == rhs then + return keymap + end + end + + return nil +end + +describe("register_backend", function() + after_each(function() + vim.api.nvim_del_user_command "IvyFd" + + local keymap = get_keymap("n", "IvyFd") + if keymap then + vim.api.nvim_del_keymap("n", keymap.lhs) + end + end) + + it("registers a backend from a string with the default options", function() + register_backend "ivy.backends.files" + + local command = get_command "IvyFd" + assert.is_not_nil(command) + + local keymap = get_keymap("n", "IvyFd") + assert.is_not_nil(keymap) + end) + + it("allows you to override the keymap", function() + register_backend("ivy.backends.files", { keymap = "" }) + + local keymap = get_keymap("n", "IvyFd") + assert(keymap ~= nil) + assert.are.equal("", keymap.lhs) + end) + + it("allows you to pass in a hole backend module", function() + register_backend(require "ivy.backends.files") + + local command = get_command "IvyFd" + assert.is_not_nil(command) + + local keymap = get_keymap("n", "IvyFd") + assert.is_not_nil(keymap) + end) + + it("allows you to pass in a hole backend module", function() + register_backend { "ivy.backends.files", { keymap = "" } } + + local keymap = get_keymap("n", "IvyFd") + assert(keymap ~= nil) + assert.are.equal("", keymap.lhs) + end) +end) diff --git a/lua/ivy/utils.lua b/lua/ivy/utils.lua index 910e57f..40489a8 100644 --- a/lua/ivy/utils.lua +++ b/lua/ivy/utils.lua @@ -1,111 +1,113 @@ local utils = {} -- A list of all of the actions defined by ivy. The callback function can -- implement as many of them as necessary. As a minimum it should implement the -- "EDIT" action that is called on the default complete. utils.actions = { EDIT = "EDIT", CHECKPOINT = "CHECKPOINT", VSPLIT = "VSPLIT", SPLIT = "SPLIT", } utils.command_map = { [utils.actions.EDIT] = "edit", [utils.actions.CHECKPOINT] = "edit", [utils.actions.VSPLIT] = "vsplit", [utils.actions.SPLIT] = "split", } utils.existing_command_map = { [utils.actions.EDIT] = "buffer", [utils.actions.CHECKPOINT] = "buffer", [utils.actions.VSPLIT] = "vsplit | buffer", [utils.actions.SPLIT] = "split | buffer", } utils.command_finder = function(command, min) if min == nil then min = 3 end return function(input) -- Dont run the commands unless we have somting to search that wont -- return a ton of results or on some commands the command files with -- no search term if #input < min then return "-- Please type more than " .. min .. " chars --" end -- TODO(ade): Think if we want to start escaping the command here. I -- dont know if its causing issues while trying to use regex especially -- with word boundaries `input:gsub("'", "\\'"):gsub('"', '\\"')` local handle = io.popen(command .. " " .. input .. " 2>&1 || true") if handle == nil then return {} end local results = {} for line in handle:lines() do table.insert(results, { content = line }) end handle:close() return results end end utils.vimgrep_action = function() return function(item, action) -- Match file and line form vimgrep style commands local file = item:match "([^:]+):" local line = item:match ":(%d+):" -- Cant do anything if we cant find a file to go to if file == nil then return end utils.file_action()(file, action) if line ~= nil then vim.cmd(line) end end end utils.file_action = function() return function(file, action) if file == nil then return end local buffer_number = vim.fn.bufnr(file) local command if buffer_number > -1 then command = utils.existing_command_map[action] else command = utils.command_map[action] end if command == nil then vim.api.nvim_err_writeln("[IVY] The file action is unable the handel the action " .. action) return end vim.cmd(command .. " " .. utils.escape_file_name(file)) end end utils.line_action = function() return function(item) local line = item:match "^%s+(%d+):" - vim.cmd(line) + if line ~= nil then + vim.cmd(line) + end end end utils.escape_file_name = function(input) local file, _ = string.gsub(input, "([$%]\\[])", "\\%1") return file end return utils diff --git a/lua/ivy/utils_escape_spec.lua b/lua/ivy/utils_escape_spec.lua new file mode 100644 index 0000000..7a47331 --- /dev/null +++ b/lua/ivy/utils_escape_spec.lua @@ -0,0 +1,11 @@ +local utils = require "ivy.utils" + +it("will escape a dollar in the file name", function() + local result = utils.escape_file_name "/path/to/$file/$name.lua" + assert.is_same(result, "/path/to/\\$file/\\$name.lua") +end) + +it("will escape a brackets in the file name", function() + local result = utils.escape_file_name "/path/to/[file]/[name].lua" + assert.is_same(result, "/path/to/\\[file\\]/\\[name\\].lua") +end) diff --git a/lua/ivy/utils_line_action_spec.lua b/lua/ivy/utils_line_action_spec.lua new file mode 100644 index 0000000..8e4484e --- /dev/null +++ b/lua/ivy/utils_line_action_spec.lua @@ -0,0 +1,28 @@ +local utils = require "ivy.utils" +local line_action = utils.line_action() + +describe("utils line_action", function() + before_each(function() + spy.on(vim, "cmd") + end) + + it("will run the line command", function() + line_action " 4: Some text" + + assert.is_equal(#vim.cmd.calls, 1, "The `vim.cmd` function should be called once") + assert.spy(vim.cmd).was_called_with "4" + end) + + it("will run with more numbers", function() + line_action " 44: Some text" + + assert.is_equal(#vim.cmd.calls, 1, "The `vim.cmd` function should be called once") + assert.spy(vim.cmd).was_called_with "44" + end) + + it("dose not run any action if no line is found", function() + line_action "Some text" + + assert.spy(vim.cmd).was_not_called() + end) +end) diff --git a/lua/ivy/utils_line_action_test.lua b/lua/ivy/utils_line_action_test.lua deleted file mode 100644 index f68810e..0000000 --- a/lua/ivy/utils_line_action_test.lua +++ /dev/null @@ -1,39 +0,0 @@ -local utils = require "ivy.utils" -local line_action = utils.line_action() -local vim_mock = require "ivy.vim_mock" - -before_each(function() - vim_mock.reset() -end) - -it("will run the line command", function(t) - line_action " 4: Some text" - - if #vim_mock.commands ~= 1 then - t.error "`line_action` command length should be 1" - end - - if vim_mock.commands[1] ~= "4" then - t.error "`line_action` command should be 4" - end -end) - -it("will run with more numbers", function(t) - line_action " 44: Some text" - - if #vim_mock.commands ~= 1 then - t.error "`line_action` command length should be 1" - end - - if vim_mock.commands[1] ~= "44" then - t.error "`line_action` command should be 44" - end -end) - -it("dose not run any action if no line is found", function(t) - line_action "Some text" - - if #vim_mock.commands ~= 0 then - t.error "`line_action` command length should be 1" - end -end) diff --git a/lua/ivy/utils_vimgrep_action_test.lua b/lua/ivy/utils_vimgrep_action_spec.lua similarity index 50% rename from lua/ivy/utils_vimgrep_action_test.lua rename to lua/ivy/utils_vimgrep_action_spec.lua index 0c08c09..51cb55d 100644 --- a/lua/ivy/utils_vimgrep_action_test.lua +++ b/lua/ivy/utils_vimgrep_action_spec.lua @@ -1,56 +1,56 @@ local utils = require "ivy.utils" local vimgrep_action = utils.vimgrep_action() -local vim_mock = require "ivy.vim_mock" - -before_each(function() - vim_mock.reset() -end) local test_data = { { it = "will edit some file and goto the line", completion = "some/file.lua:2: This is some text", action = utils.actions.EDIT, commands = { "edit some/file.lua", "2", }, }, { it = "will skip the line if its not matched", completion = "some/file.lua: This is some text", action = utils.actions.EDIT, - commands = { "edit some/file.lua" }, + commands = { "buffer some/file.lua" }, }, { it = "will run the vsplit command", completion = "some/file.lua: This is some text", action = utils.actions.VSPLIT, - commands = { "vsplit some/file.lua" }, + commands = { "vsplit | buffer some/file.lua" }, }, { it = "will run the split command", completion = "some/file.lua: This is some text", action = utils.actions.SPLIT, - commands = { "split some/file.lua" }, + commands = { "split | buffer some/file.lua" }, }, } -for i = 1, #test_data do - local data = test_data[i] - it(data.it, function(t) - vimgrep_action(data.completion, data.action) +describe("utils vimgrep_action", function() + before_each(function() + spy.on(vim, "cmd") + end) + + after_each(function() + vim.cmd:revert() + end) + + for i = 1, #test_data do + local data = test_data[i] + it(data.it, function() + assert.is_true(#data.commands > 0, "You must assert that at least one command is run") - if #vim_mock.commands ~= #data.commands then - t.error("Incorrect number of commands run expected " .. #data.commands .. " but found " .. #vim_mock.commands) - end + vimgrep_action(data.completion, data.action) + assert.is_equal(#vim.cmd.calls, #data.commands, "The `vim.cmd` function should be called once") - for j = 1, #data.commands do - if vim_mock.commands[j] ~= data.commands[j] then - t.error( - "Incorrect command run expected '" .. data.commands[j] .. "' but found '" .. vim_mock.commands[j] .. "'" - ) + for j = 1, #data.commands do + assert.spy(vim.cmd).was_called_with(data.commands[j]) end - end - end) -end + end) + end +end) diff --git a/lua/ivy/window_spec.lua b/lua/ivy/window_spec.lua new file mode 100644 index 0000000..873504a --- /dev/null +++ b/lua/ivy/window_spec.lua @@ -0,0 +1,32 @@ +local window = require "ivy.window" +local controller = require "ivy.controller" + +describe("window", function() + before_each(function() + vim.cmd "highlight IvyMatch cterm=bold gui=bold" + window.initialize() + end) + + after_each(function() + controller.destroy() + end) + + it("can initialize and destroy the window", function() + assert.is_equal(vim.api.nvim_get_current_buf(), window.buffer) + + window.destroy() + assert.is_equal(nil, window.buffer) + end) + + it("can set items", function() + window.set_items { { content = "Line one" } } + assert.is_equal("Line one", window.get_current_selection()) + end) + + it("will set the items when a string is passed in", function() + local items = table.concat({ "One", "Two", "Three" }, "\n") + window.set_items(items) + + assert.is_equal(items, table.concat(vim.api.nvim_buf_get_lines(window.buffer, 0, -1, true), "\n")) + end) +end) diff --git a/lua/ivy/window_test.lua b/lua/ivy/window_test.lua deleted file mode 100644 index dcad637..0000000 --- a/lua/ivy/window_test.lua +++ /dev/null @@ -1,33 +0,0 @@ -local vim_mock = require "ivy.vim_mock" -local window = require "ivy.window" - -before_each(function() - vim_mock.reset() -end) - -it("can initialize and destroy the window", function(t) - window.initialize() - - t.assert_equal(10, window.get_buffer()) - t.assert_equal(10, window.buffer) - - window.destroy() - t.assert_equal(nil, window.buffer) -end) - -it("can set items", function(t) - window.initialize() - - window.set_items { { content = "Line one" } } - t.assert_equal("Line one", window.get_current_selection()) -end) - -it("will set the items when a string is passed in", function(t) - window.initialize() - - local items = table.concat({ "One", "Two", "Three" }, "\n") - window.set_items(items) - - local lines = table.concat(vim_mock.get_lines()[window.buffer], "\n") - t.assert_equal(items, lines) -end) diff --git a/plugin/ivy.lua b/plugin/ivy.lua index 383e866..cab4b9a 100644 --- a/plugin/ivy.lua +++ b/plugin/ivy.lua @@ -1,50 +1,19 @@ local controller = require "ivy.controller" -- Put the controller in to the vim global so we can access it in mappings -- better without requires. You can call controller commands like `vim.ivy.xxx`. -- luacheck: ignore vim.ivy = controller -local register_backend = function(backend) - assert(backend.command, "The backend must have a command") - assert(backend.items, "The backend must have a items function") - assert(backend.callback, "The backend must have a callback function") - - local user_command_options = { bang = true } - if backend.description ~= nil then - user_command_options.desc = backend.description - end - - local name = backend.name or backend.command - vim.api.nvim_create_user_command(backend.command, function() - vim.ivy.run(name, backend.items, backend.callback) - end, user_command_options) - - if backend.keymap ~= nil then - vim.api.nvim_set_keymap("n", backend.keymap, "" .. backend.command .. "", { nowait = true, silent = true }) - end -end - vim.paste = (function(overridden) return function(lines, phase) local file_type = vim.api.nvim_buf_get_option(0, "filetype") if file_type == "ivy" then vim.ivy.paste() else overridden(lines, phase) end end end)(vim.paste) -register_backend(require "ivy.backends.buffers") -register_backend(require "ivy.backends.files") -register_backend(require "ivy.backends.lines") -register_backend(require "ivy.backends.lsp-workspace-symbols") - -if vim.fn.executable "rg" then - register_backend(require "ivy.backends.rg") -elseif vim.fn.executable "ag" then - register_backend(require "ivy.backends.ag") -end - vim.cmd "highlight IvyMatch cterm=bold gui=bold" diff --git a/rust/finder.rs b/rust/finder.rs index f3baa97..37eff2a 100644 --- a/rust/finder.rs +++ b/rust/finder.rs @@ -1,39 +1,43 @@ use ignore::{overrides::OverrideBuilder, WalkBuilder}; use std::fs; pub struct Options { pub directory: String, } pub fn find_files(options: Options) -> Vec { let mut files: Vec = Vec::new(); let base_path = &fs::canonicalize(options.directory).unwrap(); let mut builder = WalkBuilder::new(base_path); // Search for hidden files and directories builder.hidden(false); // Don't require a git repo to use .gitignore files. We want to use the .gitignore files // wherever we are builder.require_git(false); // TODO(ade): Remove unwraps and find a good way to get the errors into the UI. Currently there // is no way to handel errors in the rust library let mut override_builder = OverrideBuilder::new(""); override_builder.add("!.git").unwrap(); override_builder.add("!.sl").unwrap(); let overrides = override_builder.build().unwrap(); builder.overrides(overrides); for result in builder.build() { - let absolute_candidate = result.unwrap(); + let absolute_candidate = match result { + Ok(absolute_candidate) => absolute_candidate, + Err(..) => continue, + }; + let candidate_path = absolute_candidate.path().strip_prefix(base_path).unwrap(); if candidate_path.is_dir() { continue; } files.push(candidate_path.to_str().unwrap().to_string()); } files } diff --git a/scripts/busted.lua b/scripts/busted.lua new file mode 100755 index 0000000..753c0e6 --- /dev/null +++ b/scripts/busted.lua @@ -0,0 +1,8 @@ +-- Script to run the busted cli tool. You can use this under nvim using be +-- below command. Any arguments can be passed in the same as the busted cli. +-- +-- ```bash +-- nvim -l scripts/busted.lua +-- ``` +vim.opt.rtp:append(vim.fn.getcwd()) +require "busted.runner" { standalone = false }