1

Regenerate nvim config

This commit is contained in:
2024-06-02 03:29:20 +02:00
parent 75eea0c030
commit ef2e28883d
5576 changed files with 604886 additions and 503 deletions

View File

@ -0,0 +1,89 @@
name: Bug Report
description: File a bug/issue
title: "bug: "
labels: [bug]
body:
- type: markdown
attributes:
value: |
**Before** reporting an issue, make sure to read the [documentation](https://github.com/folke/flash.nvim) and search [existing issues](https://github.com/folke/flash.nvim/issues). Usage questions such as ***"How do I...?"*** belong in [Discussions](https://github.com/folke/flash.nvim/discussions) and will be closed.
- type: checkboxes
attributes:
label: Did you check docs and existing issues?
description: Make sure you checked all of the below before submitting an issue
options:
- label: I have read all the flash.nvim docs
required: true
- label: I have searched the existing issues of flash.nvim
required: true
- label: I have searched the existing issues of plugins related to this issue
required: true
- type: input
attributes:
label: "Neovim version (nvim -v)"
placeholder: "0.8.0 commit db1b0ee3b30f"
validations:
required: true
- type: input
attributes:
label: "Operating system/version"
placeholder: "MacOS 11.5"
validations:
required: true
- type: textarea
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is. Please include any related errors you see in Neovim.
validations:
required: true
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1.
2.
3.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Repro
description: Minimal `init.lua` to reproduce this issue. Save as `repro.lua` and run with `nvim -u repro.lua`
value: |
-- DO NOT change the paths and don't remove the colorscheme
local root = vim.fn.fnamemodify("./.repro", ":p")
-- set stdpaths to use .repro
for _, name in ipairs({ "config", "data", "state", "cache" }) do
vim.env[("XDG_%s_HOME"):format(name:upper())] = root .. "/" .. name
end
-- bootstrap lazy
local lazypath = root .. "/plugins/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
vim.fn.system({ "git", "clone", "--filter=blob:none", "https://github.com/folke/lazy.nvim.git", lazypath, })
end
vim.opt.runtimepath:prepend(lazypath)
-- install plugins
local plugins = {
"folke/tokyonight.nvim",
{ "folke/flash.nvim", opts = {} },
-- add any other plugins here
}
require("lazy").setup(plugins, {
root = root .. "/plugins",
})
vim.cmd.colorscheme("tokyonight")
-- add anything else here
render: Lua
validations:
required: false

View File

@ -0,0 +1,36 @@
name: Feature Request
description: Suggest a new feature
title: "feature: "
labels: [enhancement]
body:
- type: checkboxes
attributes:
label: Did you check the docs?
description: Make sure you read all the docs before submitting a feature request
options:
- label: I have read all the flash.nvim docs
required: true
- type: textarea
validations:
required: true
attributes:
label: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
- type: textarea
validations:
required: true
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
- type: textarea
validations:
required: true
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
- type: textarea
validations:
required: false
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.

View File

@ -0,0 +1,72 @@
name: CI
on:
push:
pull_request:
jobs:
tests:
strategy:
matrix:
# os: [ubuntu-latest, windows-latest]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Install Neovim
shell: bash
run: |
mkdir -p /tmp/nvim
wget -q https://github.com/neovim/neovim/releases/download/nightly/nvim.appimage -O /tmp/nvim/nvim.appimage
cd /tmp/nvim
chmod a+x ./nvim.appimage
./nvim.appimage --appimage-extract
echo "/tmp/nvim/squashfs-root/usr/bin/" >> $GITHUB_PATH
- name: Run Tests
run: |
nvim --version
[ ! -d tests ] && exit 0
nvim --headless -u tests/init.lua -c "PlenaryBustedDirectory tests/ {minimal_init = 'tests/init.lua', sequential = true}"
docs:
runs-on: ubuntu-latest
needs: tests
if: ${{ github.ref == 'refs/heads/main' }}
steps:
- uses: actions/checkout@v3
- name: panvimdoc
uses: kdheepak/panvimdoc@main
with:
vimdoc: flash.nvim
version: "Neovim >= 0.8.0"
demojify: true
treesitter: true
- name: Push changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: "chore(build): auto-generate vimdoc"
commit_user_name: "github-actions[bot]"
commit_user_email: "github-actions[bot]@users.noreply.github.com"
commit_author: "github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
release:
name: release
if: ${{ github.ref == 'refs/heads/main' }}
needs:
- docs
- tests
runs-on: ubuntu-latest
steps:
- uses: google-github-actions/release-please-action@v3
id: release
with:
release-type: simple
package-name: flash.nvim
- uses: actions/checkout@v3
- name: tag stable versions
if: ${{ steps.release.outputs.release_created }}
run: |
git config user.name github-actions[bot]
git config user.email github-actions[bot]@users.noreply.github.com
git remote add gh-token "https://${{ secrets.GITHUB_TOKEN }}@github.com/google-github-actions/release-please-action.git"
git tag -d stable || true
git push origin :stable || true
git tag -a stable -m "Last Stable Release"
git push origin stable

View File

@ -0,0 +1,8 @@
tt.*
.tests
doc/tags
debug
.repro
foo.*
*.log
data

View File

@ -0,0 +1,10 @@
{
"neodev": {
"library": {
"plugins": [
"plenary.nvim",
"lazy.nvim"
]
}
}
}

View File

@ -0,0 +1,474 @@
# Changelog
## [1.18.3](https://github.com/folke/flash.nvim/compare/v1.18.2...v1.18.3) (2024-05-03)
### Bug Fixes
* **hacks:** use `vim.api.nvim__redraw` to fix the cursor instead of ffi. Fixes [#333](https://github.com/folke/flash.nvim/issues/333) ([1b128ff](https://github.com/folke/flash.nvim/commit/1b128ff527c3938460ef83fe6403ce6ce3f53b53))
## [1.18.2](https://github.com/folke/flash.nvim/compare/v1.18.1...v1.18.2) (2023-10-17)
### Bug Fixes
* **treesitter:** show warning when treesitter not available. Fixes [#261](https://github.com/folke/flash.nvim/issues/261) ([77c66d8](https://github.com/folke/flash.nvim/commit/77c66d84be3e2a2ef2e6689de668fe156af74498))
## [1.18.1](https://github.com/folke/flash.nvim/compare/v1.18.0...v1.18.1) (2023-10-16)
### Bug Fixes
* **char:** allow setting autohide=true for char mode. Fixes [#231](https://github.com/folke/flash.nvim/issues/231) ([71040c8](https://github.com/folke/flash.nvim/commit/71040c87bd64d2719727006f51f8679352eb6146))
* **jump:** send `esc` when cancelling flash. Fixes [#212](https://github.com/folke/flash.nvim/issues/212). Fixes [#233](https://github.com/folke/flash.nvim/issues/233) ([677eb59](https://github.com/folke/flash.nvim/commit/677eb59f0a94ed3b735168d9e6738723fd44796d))
* **treesitter:** include treesitter injections. Fixes [#242](https://github.com/folke/flash.nvim/issues/242) ([5fe47ba](https://github.com/folke/flash.nvim/commit/5fe47baf1be05ea34abb6912ed89a5a17cbf5661))
* **treesitter:** keep treesitter sorting when doing ;,. Fixes [#219](https://github.com/folke/flash.nvim/issues/219) ([aae8352](https://github.com/folke/flash.nvim/commit/aae83521091fac904b8584bb2dffe13420b7adc7))
## [1.18.0](https://github.com/folke/flash.nvim/compare/v1.17.3...v1.18.0) (2023-10-02)
### Features
* **char:** allow disabling clever-f motions. Fixes [#245](https://github.com/folke/flash.nvim/issues/245) ([bc1f49f](https://github.com/folke/flash.nvim/commit/bc1f49f428655b645948a3489bf0efcded6f46e6))
* enable multi window in vscode ([#230](https://github.com/folke/flash.nvim/issues/230)) ([65bd3ee](https://github.com/folke/flash.nvim/commit/65bd3ee715229fecdb5a9727e8dcd099c187622b))
* **highlight:** allow overriding flash cursor hl. Fixes [#228](https://github.com/folke/flash.nvim/issues/228) ([79d67c6](https://github.com/folke/flash.nvim/commit/79d67c6d29cd3d784eb5f1410ba057e1f1499fe9))
### Bug Fixes
* **char:** disable jump labels when reg recording/executing ([#226](https://github.com/folke/flash.nvim/issues/226)) ([503b0ab](https://github.com/folke/flash.nvim/commit/503b0ab0091776d2c40541507114ff4b2f24f5b9))
* **jump:** only open folds containing match. Fixes [#224](https://github.com/folke/flash.nvim/issues/224). Fixes [#225](https://github.com/folke/flash.nvim/issues/225) ([a74d31f](https://github.com/folke/flash.nvim/commit/a74d31ffec4a6e9feb6adc33efdba247d5d912f0))
* **search:** allow disabling multi window for search. Fixes [#198](https://github.com/folke/flash.nvim/issues/198). Fixes [#197](https://github.com/folke/flash.nvim/issues/197) ([0256d8e](https://github.com/folke/flash.nvim/commit/0256d8ecab33a9aa69fdaaf885db22e1103e2a3a))
* **state:** use actions instead of opts.actions ([30442c8](https://github.com/folke/flash.nvim/commit/30442c88b817b5d00fcbe2f88977bbd5d0221a20))
## [1.17.3](https://github.com/folke/flash.nvim/compare/v1.17.2...v1.17.3) (2023-07-20)
### Bug Fixes
* **jump:** disable operator keymaps when replaying remote. Fixes [#165](https://github.com/folke/flash.nvim/issues/165) ([9f30d48](https://github.com/folke/flash.nvim/commit/9f30d48e2f509723e59c5b0915f343ce297cf386))
## [1.17.2](https://github.com/folke/flash.nvim/compare/v1.17.1...v1.17.2) (2023-07-18)
### Bug Fixes
* **char:** only use c for first search (of count) when current=true ([c92ecbf](https://github.com/folke/flash.nvim/commit/c92ecbff98fdc8770c283aa3934349e6889195dd))
* **config:** run `setup` when using flash and it wasn't run yet. Fixes [#162](https://github.com/folke/flash.nvim/issues/162) ([c81e0d1](https://github.com/folke/flash.nvim/commit/c81e0d11b9e6e1279321e12a5d87dd3fac593854))
* **state:** feed char when incremental and no match. Fixes [#57](https://github.com/folke/flash.nvim/issues/57) ([925f733](https://github.com/folke/flash.nvim/commit/925f733a731f8ed351e47d434e3a353995761012))
## [1.17.1](https://github.com/folke/flash.nvim/compare/v1.17.0...v1.17.1) (2023-07-16)
### Bug Fixes
* **char:** fix current for tT when count=0. Fixes [#159](https://github.com/folke/flash.nvim/issues/159) ([8604b56](https://github.com/folke/flash.nvim/commit/8604b562d919772dc161ac831dd7bfa948833fdd))
* **char:** never add mappings for mapleader and maplocalleader ([6e3dab6](https://github.com/folke/flash.nvim/commit/6e3dab6b011bb7661b16e14dd4aa4215894c9291))
* **char:** never overwrite existing mappings for ; and , ([abda6b8](https://github.com/folke/flash.nvim/commit/abda6b848bb11051e6a789f8a8572da3d3840bf1))
* **char:** reset including current for tT searches. Fixes [#152](https://github.com/folke/flash.nvim/issues/152) ([9c53dad](https://github.com/folke/flash.nvim/commit/9c53dad391801acb9ce9aa49820f15f6692aec91))
* **highlight:** set hl of target to current if it's a single character only. See [#158](https://github.com/folke/flash.nvim/issues/158) ([47d147b](https://github.com/folke/flash.nvim/commit/47d147b9527025b2ee73631b098edb5798afef4b))
* **remote:** properly pass register for remote ops. Fixes [#156](https://github.com/folke/flash.nvim/issues/156) ([34cf6f6](https://github.com/folke/flash.nvim/commit/34cf6f685d2eabc8de438fdbaa41c8c17e9da459))
## [1.17.0](https://github.com/folke/flash.nvim/compare/v1.16.0...v1.17.0) (2023-07-14)
### Features
* **labels:** allow disabling reusing labels. Closes [#147](https://github.com/folke/flash.nvim/issues/147) ([4b73e61](https://github.com/folke/flash.nvim/commit/4b73e6124f4e9b44713cb85ec5db3809923d2374))
### Bug Fixes
* **char:** properly exit op mode when doing esc with ftFT and jump labels ([4731cc4](https://github.com/folke/flash.nvim/commit/4731cc47459f66f9a73d19e11ea157e105384fd6))
* **char:** set inclusive=false for FT. Fixes [#149](https://github.com/folke/flash.nvim/issues/149) ([b1af2b7](https://github.com/folke/flash.nvim/commit/b1af2b78b30e814c08840a5bb7f7ccef726ea771))
* **jump:** better way to cancel operator pending mode ([4a980ea](https://github.com/folke/flash.nvim/commit/4a980ea7fedf20c902375fe7aa1141d671b0ffa7))
## [1.16.0](https://github.com/folke/flash.nvim/compare/v1.15.0...v1.16.0) (2023-07-12)
### Features
* **fold:** show first label inside a fold on the folded text line. Fixes [#39](https://github.com/folke/flash.nvim/issues/39) ([2846324](https://github.com/folke/flash.nvim/commit/28463247f21a6e0b5486dc6d31c7ace0e43a4877))
* **jump:** open folds when jumping to a folded position. See [#39](https://github.com/folke/flash.nvim/issues/39) ([dcb494c](https://github.com/folke/flash.nvim/commit/dcb494cfa79aae32e17a44026591564793b75434))
* **search:** when nohlsearch=false, matches will now be shown after jump. Fixes [#142](https://github.com/folke/flash.nvim/issues/142) ([6e7d6c2](https://github.com/folke/flash.nvim/commit/6e7d6c26a4528a8d6a17e2d23c3f5738491d736d))
### Bug Fixes
* **repeat:** no dot repeat inside macros. Fixes [#143](https://github.com/folke/flash.nvim/issues/143) ([f7218c2](https://github.com/folke/flash.nvim/commit/f7218c2d44a8d67c5c4b40edd569c55f95754354))
## [1.15.0](https://github.com/folke/flash.nvim/compare/v1.14.0...v1.15.0) (2023-07-07)
### Features
* **search:** flash toggle in search is now permanent until you toggle again. Closes [#134](https://github.com/folke/flash.nvim/issues/134) ([7ceee0d](https://github.com/folke/flash.nvim/commit/7ceee0de7e96c7453d5f82dcfc938f08d8029703))
### Bug Fixes
* **char:** special handling for t/T at current position. Fixes [#137](https://github.com/folke/flash.nvim/issues/137) ([268bffe](https://github.com/folke/flash.nvim/commit/268bffe7b9b1b9a3a4bb64a5bc8ac0627b4b7c14))
## [1.14.0](https://github.com/folke/flash.nvim/compare/v1.13.2...v1.14.0) (2023-07-05)
### Features
* **char:** added optional multi_line=false for ftFT motions. See [#102](https://github.com/folke/flash.nvim/issues/102) ([2f92418](https://github.com/folke/flash.nvim/commit/2f924186255a56cab4cf22e13b0bc1fb906b11fa))
* **char:** option for behavior of ;, and char repeats. Closes [#124](https://github.com/folke/flash.nvim/issues/124) ([97eba7d](https://github.com/folke/flash.nvim/commit/97eba7df4454097c1f6cc447de2a4e9230831ffb))
* **search:** allow finding current ([6659a94](https://github.com/folke/flash.nvim/commit/6659a94a033c2f6fec1e142451aa264f03e5da90))
* **state:** added optional `filter` for matches by non-search matcher. See [#118](https://github.com/folke/flash.nvim/issues/118) ([780ad57](https://github.com/folke/flash.nvim/commit/780ad57dedb464bfe8361356959b3ac5aaed533d))
* **treesitter:** added `node:TSNode` to ts `Flash.Match.TS` ([1cbaff4](https://github.com/folke/flash.nvim/commit/1cbaff4a7f074c1121c89207210e4588321acd40))
### Bug Fixes
* **char:** fixed tT at current. Fixes [#128](https://github.com/folke/flash.nvim/issues/128) ([a1c8aa6](https://github.com/folke/flash.nvim/commit/a1c8aa62204d5eb2036e819f5b919b1fe4b88918))
* **jump:** move offset calc outside op mode ([69141ea](https://github.com/folke/flash.nvim/commit/69141ea571602a9202ad51fae1cfe7c1894fe036))
* **search:** count=0 ([6d1d066](https://github.com/folke/flash.nvim/commit/6d1d066e6b5fcc2ed3ca446d229c0a0d306acf17))
* take into count of multi-width characters on offset of highlights and jump ([#125](https://github.com/folke/flash.nvim/issues/125)) ([41c09fa](https://github.com/folke/flash.nvim/commit/41c09faf8588887c7c15d8ca63c9ede805437da2))
## [1.13.2](https://github.com/folke/flash.nvim/compare/v1.13.1...v1.13.2) (2023-07-02)
### Bug Fixes
* **highlight:** dont use current when rainbow is used and match == target. Fixes [#109](https://github.com/folke/flash.nvim/issues/109) ([edb82f7](https://github.com/folke/flash.nvim/commit/edb82f763ac2b63006154e9da8b6629b570de551))
## [1.13.1](https://github.com/folke/flash.nvim/compare/v1.13.0...v1.13.1) (2023-07-02)
### Bug Fixes
* **config:** dont show jumpt labels by default! Fixup. See [#103](https://github.com/folke/flash.nvim/issues/103) ([7bb89b2](https://github.com/folke/flash.nvim/commit/7bb89b20fd42037c1cd7ed8d3193081d86f8c39b))
* **highlight:** don't show the label when at cursor in same window and not a range. See [#74](https://github.com/folke/flash.nvim/issues/74) ([7a8e07e](https://github.com/folke/flash.nvim/commit/7a8e07e62ad1a378d6eca958aad90fc071d14e9c))
* **labeler:** don't label folded lines. Fixes [#39](https://github.com/folke/flash.nvim/issues/39). See [#106](https://github.com/folke/flash.nvim/issues/106) ([8af3773](https://github.com/folke/flash.nvim/commit/8af3773b7b960b053038868ea18867b94abae9c8))
## [1.13.0](https://github.com/folke/flash.nvim/compare/v1.12.0...v1.13.0) (2023-07-01)
### Features
* **config:** added `opts.config` for dynamically configuring flash. Closes [#103](https://github.com/folke/flash.nvim/issues/103) ([3829d81](https://github.com/folke/flash.nvim/commit/3829d81fd6f5f6ca784bb9628a1b99298b88a3af))
### Bug Fixes
* **state:** use strchars instead of strcharlen for compat 0.8.2. Fixes [#105](https://github.com/folke/flash.nvim/issues/105) ([33e0793](https://github.com/folke/flash.nvim/commit/33e0793a614735a3fffb93763c4c9bd81b55433b))
## [1.12.0](https://github.com/folke/flash.nvim/compare/v1.11.0...v1.12.0) (2023-06-30)
### Features
* **state:** added support for custom keymaps and lmap. See [#66](https://github.com/folke/flash.nvim/issues/66) ([9aa7805](https://github.com/folke/flash.nvim/commit/9aa78057cf13dde3d39bf25cfe5caf092083cc0c))
### Bug Fixes
* **labeler:** fixed calculating skip labels for mbyte keymaps. See [#66](https://github.com/folke/flash.nvim/issues/66) ([2da635f](https://github.com/folke/flash.nvim/commit/2da635f54b81538a1e12b4859bc292d7d3e5f1b9))
* **treesitter:** added support for Nvim 0.8.0. Fixes [#100](https://github.com/folke/flash.nvim/issues/100) ([67ed44d](https://github.com/folke/flash.nvim/commit/67ed44d5efd2d05b49af861859740eedf3a076b6))
* **treesitter:** some nodes were missing ([7f4e25f](https://github.com/folke/flash.nvim/commit/7f4e25fae0fa1d3adfeb3e3e87fba9ff914032a0))
## [1.11.0](https://github.com/folke/flash.nvim/compare/v1.10.1...v1.11.0) (2023-06-29)
### Features
* **char:** hide flash when doing an ftFT search while yanking. Closes [#6](https://github.com/folke/flash.nvim/issues/6) ([feda1d5](https://github.com/folke/flash.nvim/commit/feda1d5a98a1705e86966e62a052661a7369b3c0))
* **char:** optional jump labels for ftFT searches ([d2ad5e0](https://github.com/folke/flash.nvim/commit/d2ad5e0d776a89ee424a7e0cd4364ec5dbf11dc4))
* **char:** support alternative f/F/t/T/;/, keymaps (fix [#96](https://github.com/folke/flash.nvim/issues/96)) ([#99](https://github.com/folke/flash.nvim/issues/99)) ([c0c006a](https://github.com/folke/flash.nvim/commit/c0c006a7bb694b4cec9a5f40e632f871b478e0d0))
* **label:** added `opts.label.format` for formatting rendered labels. Closes [#84](https://github.com/folke/flash.nvim/issues/84) ([2d3e7b9](https://github.com/folke/flash.nvim/commit/2d3e7b90c568083e9857b100dc2570d269da0a0c))
* **labeler:** allow excluding certain labels with a specific case ([6b255d3](https://github.com/folke/flash.nvim/commit/6b255d37505445da3db6fae5d79dff63529cd222))
* **pos:** Pos can now be initialized with window or current window cursor ([7a05cd5](https://github.com/folke/flash.nvim/commit/7a05cd5dadb78b8d475526157e464f24d14ff5b2))
* **search:** you can now `toggle` flash while using regular search ([e761182](https://github.com/folke/flash.nvim/commit/e761182f6c79ff5f88c877729465ece05b01c65a))
* **state:** custom char actions ([4f44bb4](https://github.com/folke/flash.nvim/commit/4f44bb454df0c6f598e75cd8501a1eb8e1bd2df5))
### Bug Fixes
* **hacks:** make sure to render the cursor before getchar ([2b328d1](https://github.com/folke/flash.nvim/commit/2b328d121c2b56cf25e1eb9ba92c7459beb241be))
* **highlight:** never put an extmark on the current cursor position ([8434130](https://github.com/folke/flash.nvim/commit/843413028843d1c3ce29449fe9ff62af8f642540))
* **highlight:** use current hl if pos == label pos ([56531ee](https://github.com/folke/flash.nvim/commit/56531ee85d919e787dbb247aabedb5d3dd0b7bd1))
* **jump:** replace opfunc by noop to properly cancel custom operators. Fixes [#93](https://github.com/folke/flash.nvim/issues/93) ([40b2bcb](https://github.com/folke/flash.nvim/commit/40b2bcbb05f1452f2ee7d21b79ce8ba77ea6cc94))
* **jump:** temporarily set selection=inclusive. Closes [#81](https://github.com/folke/flash.nvim/issues/81) ([5c9505a](https://github.com/folke/flash.nvim/commit/5c9505a19edcbb236d367282584ed5f02ccd4fb4))
* **labeler:** fixed label distance calculation ([1d941de](https://github.com/folke/flash.nvim/commit/1d941de722564a8ac2f07c2df262a48c49c1cdb9))
* **labeler:** put original pattern in a `\%()` group. Fixes some skip label issues ([6102a7c](https://github.com/folke/flash.nvim/commit/6102a7c0e93dbcf592a7ed2b7a2a5c2a84c5033e))
* **labeler:** skip all labels on invalid regex. Fixes [#94](https://github.com/folke/flash.nvim/issues/94) ([1fff746](https://github.com/folke/flash.nvim/commit/1fff746049253b10a008d60e1752065a98fd8614))
* **remote:** use nvim_input instead of nvim_feedkeys for clearing op mode ([c90eae5](https://github.com/folke/flash.nvim/commit/c90eae5172a00551d51883cf8b67306a812a713f))
* **search:** correctly set match end pos for multi byte characters. Fixes [#90](https://github.com/folke/flash.nvim/issues/90) ([0193d52](https://github.com/folke/flash.nvim/commit/0193d52af38d228b79569c62e06ee36b77a1a85e))
* **treesitter:** ignore windows without ts parser. Fixes [#91](https://github.com/folke/flash.nvim/issues/91) ([13022c0](https://github.com/folke/flash.nvim/commit/13022c09fa30fb03d14110a380238f6a75b42ab4))
## [1.10.1](https://github.com/folke/flash.nvim/compare/v1.10.0...v1.10.1) (2023-06-27)
### Bug Fixes
* **highlight:** apply after labels and then before ([4439fca](https://github.com/folke/flash.nvim/commit/4439fca240a54ef4d4537102668285e9cbb6f23c))
* **highlight:** correctly order after labels at the same column ([b096797](https://github.com/folke/flash.nvim/commit/b096797b64f56357c40222f5a3cff6f25ac3b5dc))
* **highlight:** make sure col is not negative with label.before = true ([cbce7f9](https://github.com/folke/flash.nvim/commit/cbce7f923c74fb75be030273c0d49f6a3447a95f))
* **prompt:** never show the prompt when in regular search ([51149ba](https://github.com/folke/flash.nvim/commit/51149ba2e6bcba0a28e67b9654450835437a2914))
* **rainbow:** stable rainbow label highlight groups ([937df4f](https://github.com/folke/flash.nvim/commit/937df4f097781e3e91594bf69425f3e74044b711))
## [1.10.0](https://github.com/folke/flash.nvim/compare/v1.9.0...v1.10.0) (2023-06-27)
### Features
* **highlight:** added optional rainbow labels. Disabled by default. Useful for Treesitter ranges. ([#74](https://github.com/folke/flash.nvim/issues/74)) ([ffb865b](https://github.com/folke/flash.nvim/commit/ffb865b1a60732d9ce2c9bffe3fb6724e1004ebb))
### Bug Fixes
* **char:** force before=false with f, F motion ([#75](https://github.com/folke/flash.nvim/issues/75)) ([40313ec](https://github.com/folke/flash.nvim/commit/40313ecf3140264b6e9d9611a3832a32e5ab7a46))
* **search:** fixup for search commmands ([0f2d53d](https://github.com/folke/flash.nvim/commit/0f2d53d63e9d90f7a310509fbf4e98fbe21be56e))
## [1.9.0](https://github.com/folke/flash.nvim/compare/v1.8.0...v1.9.0) (2023-06-26)
### Features
* **treesitter:** added treesitter search to label ts nodes around search matches ([6f791d4](https://github.com/folke/flash.nvim/commit/6f791d4709a2c8ef2373302d3a067ae45fdc2f8d))
### Bug Fixes
* added unicode support for labels/skips and fuzzy search. See [#66](https://github.com/folke/flash.nvim/issues/66) ([2528752](https://github.com/folke/flash.nvim/commit/2528752b7efbf3f67cce8b9d0d75ee769f72c01e))
* **state:** restore window views on esc or ctrl-c ([7b21dfd](https://github.com/folke/flash.nvim/commit/7b21dfddcf7ccc4fb665ca0db80810210f8cde7c))
* **treesitter:** add incremental = false to default settings of treesitter ([1cf706f](https://github.com/folke/flash.nvim/commit/1cf706f342bea4447c2f8ac13c2fab9df060ce1e))
## [1.8.0](https://github.com/folke/flash.nvim/compare/v1.7.0...v1.8.0) (2023-06-26)
### Features
* added prompt window that shows pattern during jump (can be disabled) ([3fff703](https://github.com/folke/flash.nvim/commit/3fff7033f53b8f0714efd0dd56b03aa3f22c6376))
* **api:** allow a match to disable getting a label ([ea56cea](https://github.com/folke/flash.nvim/commit/ea56ceaea4760b2031719d8e5eb1b6231ef9f43c))
* **api:** allow a match to enable/disable highlight ([38eca97](https://github.com/folke/flash.nvim/commit/38eca97c8bdbbbd7be64b562eeb9f964cf8bc145))
* **ffi:** added `mappings_enabled` ([6f6af15](https://github.com/folke/flash.nvim/commit/6f6af15b491bee14460873fe63fc7b20e7c73dd8))
* **hacks:** added support for detecting user input waiting ([81c610a](https://github.com/folke/flash.nvim/commit/81c610acd374b40fc7a7fa4b493b1b9783d3d52d))
* **highlight:** added option to disable distance based labeling ([ad9212f](https://github.com/folke/flash.nvim/commit/ad9212f28ef37e893a5a4113f8757052b2035c36))
* **highlight:** show fake cursor in all windows when flash is active ([471b165](https://github.com/folke/flash.nvim/commit/471b165722ae5db4ddad7cbaf1d351127fb55529))
* **highlight:** when running in vscode, set default hl groups to something that works ([d4c30b1](https://github.com/folke/flash.nvim/commit/d4c30b169f01b8108c5bc38e230a975408133603))
* **jump:** added jump offset ([0f2dfac](https://github.com/folke/flash.nvim/commit/0f2dfaca329ed9a7db9e5062d964492cf51765eb))
* **jump:** added options for remote operator pending mode ([436d1f4](https://github.com/folke/flash.nvim/commit/436d1f402a696733b8a1512072bbd0ac8da72cea))
* **jump:** remote operator pending operations will now always return to the original window ([c11d0d1](https://github.com/folke/flash.nvim/commit/c11d0d15660ce309c733982b2c34cd54c9c9d9f0))
* **label:** minimum pattern length to show labels. Closes [#68](https://github.com/folke/flash.nvim/issues/68) ([2c2302a](https://github.com/folke/flash.nvim/commit/2c2302a3eae1dc72d2140c58974e2f73df41556d))
* matcher function now has a from/to opts param ([1cb669d](https://github.com/folke/flash.nvim/commit/1cb669d2ce074ea39722da9fec6b0c2686b3b484))
* **remote_op:** allow setting motion to `nil` to automatically start a new motion when needed ([259062d](https://github.com/folke/flash.nvim/commit/259062ddc47f9de11e0e498cd58040705d7b6f5c))
* **remote:** implement remote using new `remote_op` options ([51f5c35](https://github.com/folke/flash.nvim/commit/51f5c352db8791f4218e19cc7fa40948cdda9647))
* searches can now be continued. Closes [#54](https://github.com/folke/flash.nvim/issues/54) ([487aa52](https://github.com/folke/flash.nvim/commit/487aa52956fdf79ba545151227b0ad39c5276c69))
* **state:** added support for restoring all window views and current window ([01736c0](https://github.com/folke/flash.nvim/commit/01736c01eb43dcf497a946689c7f434b1d13b4a8))
* **util:** luv check that does something when something finishes ([a3643eb](https://github.com/folke/flash.nvim/commit/a3643eb5424c12b5abc7b08a74d0d53fa5a29af0))
* **vscode:** make flash work properly in vscode by updating/changing the default config. Fixes [#58](https://github.com/folke/flash.nvim/issues/58) ([fa72836](https://github.com/folke/flash.nvim/commit/fa72836760417436cfe8e33ee74edaefd8ee9e00))
### Bug Fixes
* **config:** process modes in correct order. Fixes [#50](https://github.com/folke/flash.nvim/issues/50) again ([919cbe4](https://github.com/folke/flash.nvim/commit/919cbe49b66758cf57529847c396e718a9883de0))
* disable prompt on vscode ([f93b33d](https://github.com/folke/flash.nvim/commit/f93b33d736fb2eb6f28526ab465cfe7f32e7d96f))
* **jump:** fixup to always use a motion for remote ops ([11fa883](https://github.com/folke/flash.nvim/commit/11fa8833c62175a88fc35c50f1d23d5002d20fda))
* **jump:** improved operator pending mode for jumps ([16f785f](https://github.com/folke/flash.nvim/commit/16f785f26e74b8f0b49901356c57cda2a06379f5))
* **jump:** operator pending mode for remote jumps now behaves correctly ([cb24e66](https://github.com/folke/flash.nvim/commit/cb24e667ea58cfa7ea9df9fdf41bb6a26ea13da1))
* **remote:** make sure opts always exists ([7083750](https://github.com/folke/flash.nvim/commit/7083750697dea16b3943ca8a92c958acd83c2126))
* **search:** added support for search-commands. Fixes [#67](https://github.com/folke/flash.nvim/issues/67) ([7a59c42](https://github.com/folke/flash.nvim/commit/7a59c4239ed11ca3ec91cd7544535d836f09eb20))
## [1.7.0](https://github.com/folke/flash.nvim/compare/v1.6.0...v1.7.0) (2023-06-24)
### Features
* **config:** allow mode inheritance. Closes [#50](https://github.com/folke/flash.nvim/issues/50) ([3deefe8](https://github.com/folke/flash.nvim/commit/3deefe88e02e68c163c320614be1727fa887cd65))
* **jump:** added option to force inclusive/exclusive. Closes [#49](https://github.com/folke/flash.nvim/issues/49) ([e71efbf](https://github.com/folke/flash.nvim/commit/e71efbfbc73df21d3e79d30c4c27bd29892c216c))
* **remote:** peoperly deal with c for remote. Will jump back when leaving insert mode ([1075013](https://github.com/folke/flash.nvim/commit/10750139d3d4f2fb6c7bb8cc33aef988a7b26b7c))
* **state:** allow passing a callable object as matcher ([f49fa9c](https://github.com/folke/flash.nvim/commit/f49fa9cbddd6a30c59420892e09f57f391bd9516))
### Bug Fixes
* **cache:** allow current window to be excluded ([770763c](https://github.com/folke/flash.nvim/commit/770763ce2d2b4c340249cb7000de81c2085438c8))
* **cache:** fixup for window selection ([ed3bec6](https://github.com/folke/flash.nvim/commit/ed3bec6da9b92cee4954bfb71c4e71d06406191c))
* **char:** add group to autocmd ([fc08d27](https://github.com/folke/flash.nvim/commit/fc08d279ddb92ba2323684a2077aa7797384fc3c))
* **remote:** properly restore remote window as well. Also remove the `normal! o` ([587a243](https://github.com/folke/flash.nvim/commit/587a2436f84301b84937242657dcc03be4a80702))
### Performance Improvements
* **remote:** restore views on TextYankPost ([d4dadc8](https://github.com/folke/flash.nvim/commit/d4dadc8fae53ded2a51a2ca0a9d82889e148e0b7))
## [1.6.0](https://github.com/folke/flash.nvim/compare/v1.5.0...v1.6.0) (2023-06-24)
### Features
* **config:** pattern can now have a `max_length`. When length is reached, labels are no longer skipped. When it exceeds, either a jump is followed or the search is ended ([bd9dbee](https://github.com/folke/flash.nvim/commit/bd9dbee041296a582faa6dfe25e1af87d65614c7))
### Bug Fixes
* **config:** exclude noice by default ([bc9a599](https://github.com/folke/flash.nvim/commit/bc9a5992b947ae84b5c1458f0b117abda1b61154))
* **repeat:** make sure repeat is enabled for char searches. Fixes [#40](https://github.com/folke/flash.nvim/issues/40) ([219f0c0](https://github.com/folke/flash.nvim/commit/219f0c09b664257a7d9b46023bcb24563ae49832))
* **state:** always reposition the cursor on incremental mode ([81e38d6](https://github.com/folke/flash.nvim/commit/81e38d604d285d835a9186f82e28a302bc048128))
## [1.5.0](https://github.com/folke/flash.nvim/compare/v1.4.1...v1.5.0) (2023-06-23)
### Features
* added remote plugin ([fb50450](https://github.com/folke/flash.nvim/commit/fb5045044f28caf08ca6d89e9fe40874138faeef))
* flash remote. thank you [@max397574](https://github.com/max397574)! ([809ea4f](https://github.com/folke/flash.nvim/commit/809ea4f804d831ca5ff26c94b8d409ad9dfec8eb))
### Bug Fixes
* **char:** always stop highlights in insert mode ([64e5129](https://github.com/folke/flash.nvim/commit/64e51292e83e7ce409248fd07ff00b51a993a6c0))
## [1.4.1](https://github.com/folke/flash.nvim/compare/v1.4.0...v1.4.1) (2023-06-23)
### Bug Fixes
* **char:** don't repeat on motion char when executing a macro. See [#34](https://github.com/folke/flash.nvim/issues/34) ([674cfb4](https://github.com/folke/flash.nvim/commit/674cfb43e5424a5405661ba632810bacfc0a9c37))
## [1.4.0](https://github.com/folke/flash.nvim/compare/v1.3.0...v1.4.0) (2023-06-23)
### Features
* **char:** tfTF now behave like clever-f when repeating the motion. Fixes [#26](https://github.com/folke/flash.nvim/issues/26) ([97c3a99](https://github.com/folke/flash.nvim/commit/97c3a993e60ebdd42c7671af07620f705ee6378f))
* **config:** allow custom window filters. Added non-focusable windows by default ([e6ee00d](https://github.com/folke/flash.nvim/commit/e6ee00d4e76edac8cbcabe0f442a5ec34450d1f6))
### Bug Fixes
* **config:** dont show flash in cmp_menu ([29c35de](https://github.com/folke/flash.nvim/commit/29c35dec5f81504ee63a39fec90597222620af0a))
* **treesitter:** always disable incremental mode for treesitter. Fixes [#27](https://github.com/folke/flash.nvim/issues/27) ([6e84716](https://github.com/folke/flash.nvim/commit/6e8471673a7158a8820986f6aad770a912a66eed))
## [1.3.0](https://github.com/folke/flash.nvim/compare/v1.2.0...v1.3.0) (2023-06-22)
### Features
* **char:** optionally disable some ftFT keymaps ([3e27d9a](https://github.com/folke/flash.nvim/commit/3e27d9ab07b9363b0ecb94645eae38909f7baa5a))
* **config:** show labels for current jump target by default ([0dcc00e](https://github.com/folke/flash.nvim/commit/0dcc00ea6a3b312b8e081f3f582adc26a4721ac7))
* **search:** optional trigger character. Not recommended. Fixes [#21](https://github.com/folke/flash.nvim/issues/21) ([cb0977c](https://github.com/folke/flash.nvim/commit/cb0977cd0f7cec4573ee1210edc2032739866b2b))
### Bug Fixes
* **char:** fixup for keys ([81469aa](https://github.com/folke/flash.nvim/commit/81469aaf3ccf15d7c942bbd9144f2c06f68fe1ee))
* **treesitter:** properly deal with nodes ending at col 0. Fixes [#17](https://github.com/folke/flash.nvim/issues/17) ([6cd4414](https://github.com/folke/flash.nvim/commit/6cd44145f75392fbfe67700b59517dbf8324bd21))
* **treesitter:** removed debug print ([0fabd1b](https://github.com/folke/flash.nvim/commit/0fabd1b4ddea5754576ccc09a515867a3ac129ce))
## [1.2.0](https://github.com/folke/flash.nvim/compare/v1.1.0...v1.2.0) (2023-06-21)
### Features
* added example that matches beginning of words only ([1e2c61d](https://github.com/folke/flash.nvim/commit/1e2c61d8db882cc001fcebff9eba2549336ce87a))
* **config:** setting to disable uppercase labels. Fixes [#11](https://github.com/folke/flash.nvim/issues/11) ([13d7b3e](https://github.com/folke/flash.nvim/commit/13d7b3e70cadc7e4d64f818a04fbca2b33ac1d4f))
* **labeler:** reuse only lowercase labels by default. See [#11](https://github.com/folke/flash.nvim/issues/11) ([8f0b9ed](https://github.com/folke/flash.nvim/commit/8f0b9ed656d7b92eb0d60c34b6a5bd3803cc0e0b))
## [1.1.0](https://github.com/folke/flash.nvim/compare/v1.0.0...v1.1.0) (2023-06-21)
### Features
* added config.jump.autojump. Fixes [#5](https://github.com/folke/flash.nvim/issues/5) ([1808d3e](https://github.com/folke/flash.nvim/commit/1808d3ebb6ea5810957b8f8e32aab8f4e9e7f14c))
* added custom actions on label select ([eb0769f](https://github.com/folke/flash.nvim/commit/eb0769ff38001ed3eead9e54289b7f63387e1525))
* added example plugin that shows a diagnostic at a certain label without moving the cursor ([7a9bd11](https://github.com/folke/flash.nvim/commit/7a9bd118a3b4d2829d4718c26d8af21b36ebfb87))
### Bug Fixes
* **config:** get mode opts from options instead of defaults. Fixes [#4](https://github.com/folke/flash.nvim/issues/4) ([41fab4c](https://github.com/folke/flash.nvim/commit/41fab4cb225d9233fec7987bb1445c9768d84caf))
* **diag:** always hide when done ([226c634](https://github.com/folke/flash.nvim/commit/226c634e3db6f02eb734d37c16d729bae41a77ef))
* **jump:** register and history should use pattern.search instead of pattern. Fixes [#7](https://github.com/folke/flash.nvim/issues/7) ([a11cf6a](https://github.com/folke/flash.nvim/commit/a11cf6ad205dd2493d2af6643bc20bef925004f5))
* **treesitter:** make treesitter plugin work with custom labels. Fixes [#9](https://github.com/folke/flash.nvim/issues/9) ([3fac625](https://github.com/folke/flash.nvim/commit/3fac6253fd59e7c32300e6209c8f1e60ea8a3c81))
## 1.0.0 (2023-06-21)
### Features
* abort_pattern can now be false ([e036667](https://github.com/folke/flash.nvim/commit/e0366678e337df4a93c0704e77a6909e617950c3))
* add option to save loc to jumplist before jump ([0aae816](https://github.com/folke/flash.nvim/commit/0aae816ef419ad4554a784a07fe239aeee9a6934))
* added char searches, f, F, t, T ([06839d8](https://github.com/folke/flash.nvim/commit/06839d8ac7f2ca42b639fc8f90e2c655234bba9a))
* added config for forward/wrap ([b9649bd](https://github.com/folke/flash.nvim/commit/b9649bd226da89bcbef7fb6b27e5d3a08d0fe6b4))
* added config.search.regex ([bda1be0](https://github.com/folke/flash.nvim/commit/bda1be00bca62d7ebd9de4c7848e7c70a65f2f91))
* added ffi based searcher. Finally 100% correct end pos for matches ([46b41d1](https://github.com/folke/flash.nvim/commit/46b41d13d6943443c20b3bf87fdf8eb495fee4c2))
* added option to label the first match ([63b75ed](https://github.com/folke/flash.nvim/commit/63b75ed8dcaec7efaf6e67e3913b59f2e614f043))
* added optional backdrop ([2172a90](https://github.com/folke/flash.nvim/commit/2172a907aeba4a3961e399044a2f4ca1087e044d))
* added support for label offsets and label styles ([3e9f630](https://github.com/folke/flash.nvim/commit/3e9f630ce04bdda14669592bc5d36af594077e95))
* added treesitter command ([fd9bd80](https://github.com/folke/flash.nvim/commit/fd9bd8015a7df2b8aedc294bc517264837d218f9))
* advance for results ([9d70126](https://github.com/folke/flash.nvim/commit/9d70126e09b20125752a43c1e26041eecc4f721c))
* allow to always render search highlight to prevent flickering when updating ui ([ff0e25f](https://github.com/folke/flash.nvim/commit/ff0e25f63ae98f7ab2735293a40f02e8cfc85d2a))
* **charsearch:** close on &lt;esc&gt; ([ee3228a](https://github.com/folke/flash.nvim/commit/ee3228af6b82204cb03c317526a0212229953272))
* **charsearch:** make char search dot repeatable ([91485c1](https://github.com/folke/flash.nvim/commit/91485c12b2685bdde097b2351725e973cc2e1274))
* dont stabalize labels for treesitter ([b20ad86](https://github.com/folke/flash.nvim/commit/b20ad8652f34a477f6bdab912258b176aeebdd0d))
* expose commands on main module ([70130d2](https://github.com/folke/flash.nvim/commit/70130d29a3c4c8d90d96caae5871d0cc19e3f283))
* fuzzy matching ([7407dd6](https://github.com/folke/flash.nvim/commit/7407dd679c90986dff09b22a690feb52aa5ea31a))
* highlight groups config ([313e252](https://github.com/folke/flash.nvim/commit/313e252ecfd3252d2e39d7c012b0674388d65f8d))
* **highlight:** added support for before/after labels ([d0133d2](https://github.com/folke/flash.nvim/commit/d0133d2966695f063f8909a0d80a97cd90d2848c))
* **highlight:** allow diffrerent namespaces for highlight ([2649b18](https://github.com/folke/flash.nvim/commit/2649b1888fd84d1cee0ab3d5fdc5e82c8a5f391c))
* initial version ([22913c6](https://github.com/folke/flash.nvim/commit/22913c65a1c960e3449c813824351abbdb327c7b))
* jump position (start, end or range) ([335a5a9](https://github.com/folke/flash.nvim/commit/335a5a91222680f92c585c16d94d183a57b13c8d))
* labels are now skipped based on regex searches to be able to fully support regex patterns ([e704d88](https://github.com/folke/flash.nvim/commit/e704d8846fd2d8189f127f2b080812ed2518fdc4))
* lazy require ([171b9ff](https://github.com/folke/flash.nvim/commit/171b9ff3034b2afb5ad9a0420a906a8c597037ba))
* make all the things repeatable without needing `expr=true` ([ec3a8ac](https://github.com/folke/flash.nvim/commit/ec3a8ac3ebfc9957c65620bcae7d91ed38a334b2))
* much improved repeat api ([2f76471](https://github.com/folke/flash.nvim/commit/2f76471f3a178234a3b08a6ae5ca9f8082bacc46))
* multiple modes ([ed1150f](https://github.com/folke/flash.nvim/commit/ed1150f2cabcca526894423de8fda74d756a0cff))
* **pattern:** custom pattern functions ([b9e13f2](https://github.com/folke/flash.nvim/commit/b9e13f2c8cf603e70d7eff410ffbd88c8611d6d0))
* **repeat:** show warning when keymap expr didn't execute. probably because expr=true was not used ([789d3b2](https://github.com/folke/flash.nvim/commit/789d3b22610fe8f45f7451afac5b1921db852dd6))
* stable labels ([3e6b345](https://github.com/folke/flash.nvim/commit/3e6b345f590c70c83ccbe720afc268ba9ba3b442))
* **state:** proper support for incremental search ([8a0fa11](https://github.com/folke/flash.nvim/commit/8a0fa1147cfad21b6576ee4d9320de6e78b1c24c))
* **state:** state will now automatically updated on changedtick or when buf changes ([60193cb](https://github.com/folke/flash.nvim/commit/60193cb3aa384938bd7b9be8d5b594c0ebe0c867))
* **state:** update matcher when view changed ([9f4dc50](https://github.com/folke/flash.nvim/commit/9f4dc506987a9381d67e3e602e9950a622c76276))
* treesitter node jumping ([119643f](https://github.com/folke/flash.nvim/commit/119643fd672a959233da3b1c3b61de965dfe765b))
* **treesitter:** ; & , to expand/descrease selection ([6551d97](https://github.com/folke/flash.nvim/commit/6551d970d270bda2b6bf9be09944196d8782a329))
* **treesitter:** allow custom options ([d9d5e75](https://github.com/folke/flash.nvim/commit/d9d5e7558e11e1cdb9a48c87e442444664b3c0cf))
* util module for dot-repeat ([e6f02b1](https://github.com/folke/flash.nvim/commit/e6f02b15608b625266f1564b8005c36d56f7fa71))
### Bug Fixes
* allow space in string ([f1b8691](https://github.com/folke/flash.nvim/commit/f1b86913daa85aef94fae07e03cab8ccf7f9137f))
* calculate target in update ([f3f915a](https://github.com/folke/flash.nvim/commit/f3f915ac0b5c4ff4598dd73b65cff9f9c0d3e57b))
* **charsearch:** inclusive/exclusive operator pending fix ([fb1867c](https://github.com/folke/flash.nvim/commit/fb1867c908e488a7dbe1a83f7cad57a826bf977f))
* **charsearch:** mode ([b8c18ba](https://github.com/folke/flash.nvim/commit/b8c18baad82145fe097db4d13440d44a9005f30d))
* **config:** register and nohlsearch are disables by default ([f20d2f8](https://github.com/folke/flash.nvim/commit/f20d2f8d34142ec1674284f582e57f6f66a99cd8))
* dont set search register by default ([f7352f7](https://github.com/folke/flash.nvim/commit/f7352f7c7e90e3e0b5818b398d543e2146f045ad))
* fixup for first -&gt; current ([43b96c6](https://github.com/folke/flash.nvim/commit/43b96c69d7f7fd97f5c9ec316cf8ee3c30badc48))
* **highlight:** highlight each line of the backdrop separately to fix extmark priorities ([08bf4f6](https://github.com/folke/flash.nvim/commit/08bf4f6fad136743c6791f6db4659f314fe69104))
* **highlight:** proper nvim 0.10.0 check for inline extmarks ([6da8904](https://github.com/folke/flash.nvim/commit/6da8904ed698069395baab49b168b37b0a35b839))
* **highlight:** set cursorline hl group ([8715685](https://github.com/folke/flash.nvim/commit/8715685cd24e5d5727442063ce7e347bb0b567b7))
* **init:** pass opts to config ([0627e2f](https://github.com/folke/flash.nvim/commit/0627e2f09e9a7b26d8755d8e4994e38cfdd58ba5))
* **jump:** check pattern for jump target ([d29d5fc](https://github.com/folke/flash.nvim/commit/d29d5fc41dcbe6e7c751c30d28b362400f45f870))
* **jump:** dont change ordering of matches when calculating labels ([8611eab](https://github.com/folke/flash.nvim/commit/8611eaba93c080175026dbd41fac9a7a9e535637))
* **jump:** fix inclusive/excusive for operator pending mode ([99c99a7](https://github.com/folke/flash.nvim/commit/99c99a75754f107eef0cbc23f4745e7c0d784848))
* **jump:** make it all work in operator pending mode ([1005faa](https://github.com/folke/flash.nvim/commit/1005faa1c21dcaa37232fd93c2ef7c71fc3b3099))
* **labeler:** dont include end_pos to re-use stable labels ([dadca0e](https://github.com/folke/flash.nvim/commit/dadca0e75335dd9e3083ea11cd41f1d197ebe1a7))
* **labels:** fixed some edge cases regarding labels ([124d1b6](https://github.com/folke/flash.nvim/commit/124d1b6900b30f5a2e1c60bc6a4ac0e1a0de889a))
* **matcher:** match end_pos when finding relative to another match ([0794ba2](https://github.com/folke/flash.nvim/commit/0794ba238ada4ab820940a63dbd54f29679d10be))
* **matcher:** ordering ([e46a629](https://github.com/folke/flash.nvim/commit/e46a629c679a022e822a4243ad15ebcb1474412d))
* **search:** added support for match ([e3e3958](https://github.com/folke/flash.nvim/commit/e3e3958c871bf46d808605afbdcf07cafb1e98e4))
* **search:** cleanup and add search to history ([175ffd9](https://github.com/folke/flash.nvim/commit/175ffd9960fdaf65b00d00782fdc0505678e9162))
* **search:** dont add labels if too many results ([959af4e](https://github.com/folke/flash.nvim/commit/959af4e095df35a62200a35b1f3aef2e652c8dd5))
* **searcher:** don't use ignore case for labels and skip both upper/lower when needed ([1b48511](https://github.com/folke/flash.nvim/commit/1b48511efa0834deb07461b3e076c8bafb66d876))
* **searcher:** finally was able to properly fix finding ends of matches ([4251741](https://github.com/folke/flash.nvim/commit/4251741114187823b94957dfad40e7dcfa82ac2d))
* **searcher:** skip all labels when pattern ends with escape character ([530038d](https://github.com/folke/flash.nvim/commit/530038d05925373feddb4742dcf742401532ed69))
* **searcher:** use vim.regex to get match end and added support for multi-line ([ffcdf20](https://github.com/folke/flash.nvim/commit/ffcdf20d7ff15117a984244e1258794fef10efe8))
* **search:** properly deal with invalid patterns ([46d6655](https://github.com/folke/flash.nvim/commit/46d6655891238b569ffa8c0334f2bdae39adc21e))
* **search:** skip all labels when pattern is invalid regex ([9bb8079](https://github.com/folke/flash.nvim/commit/9bb8079c82dccccc54ec107e243f845e996a492b))
* **state:** better operator pending mode detection for search ([f53dd07](https://github.com/folke/flash.nvim/commit/f53dd076af1e2f6f9374f6c26c8f474c83c5815d))
* **state:** force update when making visible ([ada913d](https://github.com/folke/flash.nvim/commit/ada913d2a1cbdb765493419202a48addaf2c873a))
* **state:** keep states as a key in a table to prevent double work ([4a6ea98](https://github.com/folke/flash.nvim/commit/4a6ea985c88eb8503515131f422d4cb856db4b3b))
* **state:** results sorting ([9da4d28](https://github.com/folke/flash.nvim/commit/9da4d285d0d453fc9eb0f3bfcebde68be334f066))
* **state:** stop searching when max matches reached ([4245e49](https://github.com/folke/flash.nvim/commit/4245e49fb878459bb5a074c9c8023900baf321cd))
* **treesitter:** use state.pos as cursor to get nodes ([d1185ad](https://github.com/folke/flash.nvim/commit/d1185add4a6f624b150896ba4eb32855ef9e35b7))
### Performance Improvements
* cache window matches ([678532a](https://github.com/folke/flash.nvim/commit/678532a956562a53887a5dda2e4513c3ba216de9))
* lazy require/setup ([2bbf721](https://github.com/folke/flash.nvim/commit/2bbf72189c875509ac37130f56fc4cb6e0f65139))

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,705 @@
# ⚡flash.nvim
`flash.nvim` lets you navigate your code with search labels,
enhanced character motions, and Treesitter integration.
<table>
<tr>
<th>Search Integration</th>
<th>Standalone Jump</th>
</tr>
<tr>
<td>
<img src="https://github.com/folke/flash.nvim/assets/292349/e0ac4cbc-fa54-4505-8261-43ec0505518d" />
</td>
<td>
<img src="https://github.com/folke/flash.nvim/assets/292349/90af85e3-3f22-4c51-af4b-2a2488c9560b" />
</td>
</tr>
<tr>
<th><code>f</code>, <code>t</code>, <code>F</code>, <code>T</code></th>
<th>Treesitter</th>
</tr>
<tr>
<td>
<img src="https://github.com/folke/flash.nvim/assets/292349/379cb2de-8ec3-4acf-8811-d3590a5854b6" />
</td>
<td>
<img src="https://github.com/folke/flash.nvim/assets/292349/b963b05e-3d28-45ff-b43a-928a06e5f92a" />
</td>
</tr>
</table>
## ✨ Features
- 🔍 **Search Integration**: integrate **flash.nvim** with your regular
search using `/` or `?`. Labels appear next to the matches,
allowing you to quickly jump to any location. Labels are
guaranteed not to exist as a continuation of the search pattern.
- ⌨️ **type as many characters as you want** before using a jump label.
-**Enhanced `f`, `t`, `F`, `T` motions**
- 🌳 **Treesitter Integration**: all parents of the Treesitter node
under your cursor are highlighted with a label for quick selection
of a specific Treesitter node.
- 🎯 **Jump Mode**: a standalone jumping mode similar to search
- 🔎 **Search Modes**: `exact`, `search` (regex), and `fuzzy` search modes
- 🪟 **Multi Window** jumping
- 🌐 **Remote Actions**: perform motions in remote locations
-**dot-repeatable** jumps
- 📡 **highly extensible**: check the [examples](https://github.com/folke/flash.nvim#-examples)
## 📋 Requirements
- Neovim >= **0.8.0** (needs to be built with **LuaJIT**)
## 📦 Installation
Install the plugin with your preferred package manager:
[lazy.nvim](https://github.com/folke/lazy.nvim):
<!-- setup:start -->
```lua
{
"folke/flash.nvim",
event = "VeryLazy",
---@type Flash.Config
opts = {},
-- stylua: ignore
keys = {
{ "s", mode = { "n", "x", "o" }, function() require("flash").jump() end, desc = "Flash" },
{ "S", mode = { "n", "x", "o" }, function() require("flash").treesitter() end, desc = "Flash Treesitter" },
{ "r", mode = "o", function() require("flash").remote() end, desc = "Remote Flash" },
{ "R", mode = { "o", "x" }, function() require("flash").treesitter_search() end, desc = "Treesitter Search" },
{ "<c-s>", mode = { "c" }, function() require("flash").toggle() end, desc = "Toggle Flash Search" },
},
}
```
<!-- setup:end -->
> ⚠️ When creating the keymaps manually either use a lua function like
> `function() require("flash").jump() end` as the **rhs**, or a string
> like `<cmd>lua require("flash").jump()<cr>`.
> **DO NOT** use `:lua`, since that will break **_dot-repeat_**
## ⚙️ Configuration
**flash.nvim** is highly configurable. Please refer to the default settings below.
<details><summary>Default Settings</summary>
<!-- config:start -->
```lua
{
-- labels = "abcdefghijklmnopqrstuvwxyz",
labels = "asdfghjklqwertyuiopzxcvbnm",
search = {
-- search/jump in all windows
multi_window = true,
-- search direction
forward = true,
-- when `false`, find only matches in the given direction
wrap = true,
---@type Flash.Pattern.Mode
-- Each mode will take ignorecase and smartcase into account.
-- * exact: exact match
-- * search: regular search
-- * fuzzy: fuzzy search
-- * fun(str): custom function that returns a pattern
-- For example, to only match at the beginning of a word:
-- mode = function(str)
-- return "\\<" .. str
-- end,
mode = "exact",
-- behave like `incsearch`
incremental = false,
-- Excluded filetypes and custom window filters
---@type (string|fun(win:window))[]
exclude = {
"notify",
"cmp_menu",
"noice",
"flash_prompt",
function(win)
-- exclude non-focusable windows
return not vim.api.nvim_win_get_config(win).focusable
end,
},
-- Optional trigger character that needs to be typed before
-- a jump label can be used. It's NOT recommended to set this,
-- unless you know what you're doing
trigger = "",
-- max pattern length. If the pattern length is equal to this
-- labels will no longer be skipped. When it exceeds this length
-- it will either end in a jump or terminate the search
max_length = false, ---@type number|false
},
jump = {
-- save location in the jumplist
jumplist = true,
-- jump position
pos = "start", ---@type "start" | "end" | "range"
-- add pattern to search history
history = false,
-- add pattern to search register
register = false,
-- clear highlight after jump
nohlsearch = false,
-- automatically jump when there is only one match
autojump = false,
-- You can force inclusive/exclusive jumps by setting the
-- `inclusive` option. By default it will be automatically
-- set based on the mode.
inclusive = nil, ---@type boolean?
-- jump position offset. Not used for range jumps.
-- 0: default
-- 1: when pos == "end" and pos < current position
offset = nil, ---@type number
},
label = {
-- allow uppercase labels
uppercase = true,
-- add any labels with the correct case here, that you want to exclude
exclude = "",
-- add a label for the first match in the current window.
-- you can always jump to the first match with `<CR>`
current = true,
-- show the label after the match
after = true, ---@type boolean|number[]
-- show the label before the match
before = false, ---@type boolean|number[]
-- position of the label extmark
style = "overlay", ---@type "eol" | "overlay" | "right_align" | "inline"
-- flash tries to re-use labels that were already assigned to a position,
-- when typing more characters. By default only lower-case labels are re-used.
reuse = "lowercase", ---@type "lowercase" | "all" | "none"
-- for the current window, label targets closer to the cursor first
distance = true,
-- minimum pattern length to show labels
-- Ignored for custom labelers.
min_pattern_length = 0,
-- Enable this to use rainbow colors to highlight labels
-- Can be useful for visualizing Treesitter ranges.
rainbow = {
enabled = false,
-- number between 1 and 9
shade = 5,
},
-- With `format`, you can change how the label is rendered.
-- Should return a list of `[text, highlight]` tuples.
---@class Flash.Format
---@field state Flash.State
---@field match Flash.Match
---@field hl_group string
---@field after boolean
---@type fun(opts:Flash.Format): string[][]
format = function(opts)
return { { opts.match.label, opts.hl_group } }
end,
},
highlight = {
-- show a backdrop with hl FlashBackdrop
backdrop = true,
-- Highlight the search matches
matches = true,
-- extmark priority
priority = 5000,
groups = {
match = "FlashMatch",
current = "FlashCurrent",
backdrop = "FlashBackdrop",
label = "FlashLabel",
},
},
-- action to perform when picking a label.
-- defaults to the jumping logic depending on the mode.
---@type fun(match:Flash.Match, state:Flash.State)|nil
action = nil,
-- initial pattern to use when opening flash
pattern = "",
-- When `true`, flash will try to continue the last search
continue = false,
-- Set config to a function to dynamically change the config
config = nil, ---@type fun(opts:Flash.Config)|nil
-- You can override the default options for a specific mode.
-- Use it with `require("flash").jump({mode = "forward"})`
---@type table<string, Flash.Config>
modes = {
-- options used when flash is activated through
-- a regular search with `/` or `?`
search = {
-- when `true`, flash will be activated during regular search by default.
-- You can always toggle when searching with `require("flash").toggle()`
enabled = false,
highlight = { backdrop = false },
jump = { history = true, register = true, nohlsearch = true },
search = {
-- `forward` will be automatically set to the search direction
-- `mode` is always set to `search`
-- `incremental` is set to `true` when `incsearch` is enabled
},
},
-- options used when flash is activated through
-- `f`, `F`, `t`, `T`, `;` and `,` motions
char = {
enabled = true,
-- dynamic configuration for ftFT motions
config = function(opts)
-- autohide flash when in operator-pending mode
opts.autohide = opts.autohide or (vim.fn.mode(true):find("no") and vim.v.operator == "y")
-- disable jump labels when not enabled, when using a count,
-- or when recording/executing registers
opts.jump_labels = opts.jump_labels
and vim.v.count == 0
and vim.fn.reg_executing() == ""
and vim.fn.reg_recording() == ""
-- Show jump labels only in operator-pending mode
-- opts.jump_labels = vim.v.count == 0 and vim.fn.mode(true):find("o")
end,
-- hide after jump when not using jump labels
autohide = false,
-- show jump labels
jump_labels = false,
-- set to `false` to use the current line only
multi_line = true,
-- When using jump labels, don't use these keys
-- This allows using those keys directly after the motion
label = { exclude = "hjkliardc" },
-- by default all keymaps are enabled, but you can disable some of them,
-- by removing them from the list.
-- If you rather use another key, you can map them
-- to something else, e.g., { [";"] = "L", [","] = H }
keys = { "f", "F", "t", "T", ";", "," },
---@alias Flash.CharActions table<string, "next" | "prev" | "right" | "left">
-- The direction for `prev` and `next` is determined by the motion.
-- `left` and `right` are always left and right.
char_actions = function(motion)
return {
[";"] = "next", -- set to `right` to always go right
[","] = "prev", -- set to `left` to always go left
-- clever-f style
[motion:lower()] = "next",
[motion:upper()] = "prev",
-- jump2d style: same case goes next, opposite case goes prev
-- [motion] = "next",
-- [motion:match("%l") and motion:upper() or motion:lower()] = "prev",
}
end,
search = { wrap = false },
highlight = { backdrop = true },
jump = { register = false },
},
-- options used for treesitter selections
-- `require("flash").treesitter()`
treesitter = {
labels = "abcdefghijklmnopqrstuvwxyz",
jump = { pos = "range" },
search = { incremental = false },
label = { before = true, after = true, style = "inline" },
highlight = {
backdrop = false,
matches = false,
},
},
treesitter_search = {
jump = { pos = "range" },
search = { multi_window = true, wrap = true, incremental = false },
remote_op = { restore = true },
label = { before = true, after = true, style = "inline" },
},
-- options used for remote flash
remote = {
remote_op = { restore = true, motion = true },
},
},
-- options for the floating window that shows the prompt,
-- for regular jumps
prompt = {
enabled = true,
prefix = { { "⚡", "FlashPromptIcon" } },
win_config = {
relative = "editor",
width = 1, -- when <=1 it's a percentage of the editor width
height = 1,
row = -1, -- when negative it's an offset from the bottom
col = 0, -- when negative it's an offset from the right
zindex = 1000,
},
},
-- options for remote operator pending mode
remote_op = {
-- restore window views and cursor position
-- after doing a remote operation
restore = false,
-- For `jump.pos = "range"`, this setting is ignored.
-- `true`: always enter a new motion when doing a remote operation
-- `false`: use the window's cursor position and jump target
-- `nil`: act as `true` for remote windows, `false` for the current window
motion = false,
},
}
```
<!-- config:end -->
</details>
## 🚀 Usage
- **Treesitter**: `require("flash").treesitter(opts?)` opens **flash** in **Treesitter** mode
- use a jump label, or use `;` and `,` to increase/decrease the selection
- **regular search**: search as you normally do, but enhanced with jump labels.
You need to set `opts.modes.search.enabled = true`, or toggle it with `require("flash").toggle()`
- `f`, `t`, `F`, `T` motions:
- After typing `f{char}` or `F{char},` you can repeat the motion with `f`
or go to the previous match with `F` to undo a jump.
- Similarly, after typing `t{char}` or `T{char},` you can repeat the motion
with `t` or go to the previous match with `T`.
- You can also go to the next match with `;` or previous match with `,`
- Any highlights clear automatically when moving, changing buffers,
or pressing `<esc>`.
- **toggle Search**: `require("flash").toggle(boolean?)`
- toggles **flash** on or off while using regular search
- **Treesitter Search**: `require("flash").treesitter_search(opts?)` opens **flash** in **Treesitter Search** mode
- combination of **Treesitter** and **Search** modes
- do something like `yR`
- you can now start typing a search pattern.
- arround your matches, all the surrounding Treesitter nodes will be labeled.
- select a label to perform the operator on the new selection
- **remote**: `require("flash").remote(opts?)` opens **flash** in **remote** mode
- equivalent to:
```lua
require("flash").jump({
remote_op = {
restore = true,
motion = true,
},
})
```
- this is only useful in operator pending mode.
- For example, press `yr` to start yanking and open flash
- select a label to set the cursor position
- perform any motion, like `iw` or even start flash Treesitter with `S`
- the yank will be performed on the new selection
- you'll be back in the original window / position
- You can also configure the `remote_op` options by default, so that `ys`,
behaves like `yr` for remote operations
```lua
require("flash").jump({
remote_op = {
restore = true,
motion = nil,
},
})
```
- **jump**: `require("flash").jump(opts?)` opens **flash** with the given options
- type any number of characters before typing a jump label
- **VS Code**: some functionality is changed/disabled when running **flash** in **VS Code**:
- `prompt` is disabled
- `highlights` are set to different defaults that will actually work in VS Code
## 📡 API
The options for `require("flash").jump(opts?)`, are the same as
those in the config section, but can additionally have the following fields:
- `matcher`: a custom function that generates matches for a given window
- `labeler`: a custom function to label matches
You can also add labels in the `matcher` function and then set `labeler`
to an empty function `labeler = function() end`
<details><summary>Type Definitions</summary>
```typescript
type FlashMatcher = (win: number, state: FlashState) => FlashMatch[];
type FlashLabeler = (matches: FlashMatch[], state: FlashState) => void;
interface FlashMatch {
win: number;
pos: [number, number]; // (1,0)-indexed
end_pos: [number, number]; // (1,0)-indexed
label?: string | false; // set to false to never show a label for this match
highlight?: boolean; // override opts.highlight.matches for this match
}
// Check the code for the full definition
// of Flash.State at `lua/flash/state.lua`
type FlashState = {};
```
</details>
## 💡 Examples
<details><summary>Forward search only</summary>
```lua
require("flash").jump({
search = { forward = true, wrap = false, multi_window = false },
})
```
</details>
<details><summary>Backward search only</summary>
```lua
require("flash").jump({
search = { forward = false, wrap = false, multi_window = false },
})
```
</details>
<details><summary>Show diagnostics at target, without changing cursor position</summary>
```lua
require("flash").jump({
action = function(match, state)
vim.api.nvim_win_call(match.win, function()
vim.api.nvim_win_set_cursor(match.win, match.pos)
vim.diagnostic.open_float()
end)
state:restore()
end,
})
-- More advanced example that also highlights diagnostics:
require("flash").jump({
matcher = function(win)
---@param diag Diagnostic
return vim.tbl_map(function(diag)
return {
pos = { diag.lnum + 1, diag.col },
end_pos = { diag.end_lnum + 1, diag.end_col - 1 },
}
end, vim.diagnostic.get(vim.api.nvim_win_get_buf(win)))
end,
action = function(match, state)
vim.api.nvim_win_call(match.win, function()
vim.api.nvim_win_set_cursor(match.win, match.pos)
vim.diagnostic.open_float()
end)
state:restore()
end,
})
```
</details>
<details><summary>Match beginning of words only</summary>
```lua
require("flash").jump({
search = {
mode = function(str)
return "\\<" .. str
end,
},
})
```
</details>
<details><summary>Initialize flash with the word under the cursor</summary>
```lua
require("flash").jump({
pattern = vim.fn.expand("<cword>"),
})
```
</details>
<details><summary>Jump to a line</summary>
```lua
require("flash").jump({
search = { mode = "search", max_length = 0 },
label = { after = { 0, 0 } },
pattern = "^"
})
```
</details>
<details><summary>Select any word</summary>
```lua
require("flash").jump({
pattern = ".", -- initialize pattern with any char
search = {
mode = function(pattern)
-- remove leading dot
if pattern:sub(1, 1) == "." then
pattern = pattern:sub(2)
end
-- return word pattern and proper skip pattern
return ([[\<%s\w*\>]]):format(pattern), ([[\<%s]]):format(pattern)
end,
},
-- select the range
jump = { pos = "range" },
})
```
</details>
<details><summary><code>f</code>, <code>t</code>, <code>F</code>, <code>T</code> with labels</summary>
Use the options below:
```lua
{
modes = {
char = {
jump_labels = true
}
}
}
```
</details>
<details><summary>Telescope integration</summary>
This will allow you to use `s` in normal mode
and `<c-s>` in insert mode, to jump to a label in Telescope results.
```lua
{
"nvim-telescope/telescope.nvim",
optional = true,
opts = function(_, opts)
local function flash(prompt_bufnr)
require("flash").jump({
pattern = "^",
label = { after = { 0, 0 } },
search = {
mode = "search",
exclude = {
function(win)
return vim.bo[vim.api.nvim_win_get_buf(win)].filetype ~= "TelescopeResults"
end,
},
},
action = function(match)
local picker = require("telescope.actions.state").get_current_picker(prompt_bufnr)
picker:set_selection(match.pos[1] - 1)
end,
})
end
opts.defaults = vim.tbl_deep_extend("force", opts.defaults or {}, {
mappings = {
n = { s = flash },
i = { ["<c-s>"] = flash },
},
})
end,
}
```
</details>
<details><summary>Continue last search</summary>
```lua
require("flash").jump({continue = true})
```
</details>
<details>
<summary>2-char jump, similar to
<a href="https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-jump2d.md">
mini.jump2d
</a>
or
<a href="https://github.com/phaazon/hop.nvim">
HopWord (hop.nvim)
</a>
</summary>
```lua
local Flash = require("flash")
---@param opts Flash.Format
local function format(opts)
-- always show first and second label
return {
{ opts.match.label1, "FlashMatch" },
{ opts.match.label2, "FlashLabel" },
}
end
Flash.jump({
search = { mode = "search" },
label = { after = false, before = { 0, 0 }, uppercase = false, format = format },
pattern = [[\<]],
action = function(match, state)
state:hide()
Flash.jump({
search = { max_length = 0 },
highlight = { matches = false },
label = { format = format },
matcher = function(win)
-- limit matches to the current label
return vim.tbl_filter(function(m)
return m.label == match.label and m.win == win
end, state.results)
end,
labeler = function(matches)
for _, m in ipairs(matches) do
m.label = m.label2 -- use the second label
end
end,
})
end,
labeler = function(matches, state)
local labels = state:labels()
for m, match in ipairs(matches) do
match.label1 = labels[math.floor((m - 1) / #labels) + 1]
match.label2 = labels[(m - 1) % #labels + 1]
match.label = match.label1
end
end,
})
```
</details>
## 🌈 Highlights
| Group | Default | Description |
| ----------------- | ------------ | -------------- |
| `FlashBackdrop` | `Comment` | backdrop |
| `FlashMatch` | `Search` | search matches |
| `FlashCurrent` | `IncSearch` | current match |
| `FlashLabel` | `Substitute` | jump label |
| `FlashPrompt` | `MsgArea` | prompt |
| `FlashPromptIcon` | `Special` | prompt icon |
| `FlashCursor` | `Cursor` | cursor |
## 📦 Alternatives
- [leap.nvim](https://github.com/ggandor/leap.nvim)
- [lightspeed.nvim](https://github.com/ggandor/lightspeed.nvim)
- [vim-sneak](https://github.com/justinmk/vim-sneak)
- [mini.jump](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-jump.md)
- [mini.jump2d](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-jump2d.md)
- [hop.nvim](https://github.com/phaazon/hop.nvim)
- [pounce.nvim](https://github.com/rlane/pounce.nvim)
- [sj.nvim](https://github.com/woosaaahh/sj.nvim)
- [nvim-treehopper](https://github.com/mfussenegger/nvim-treehopper)
- [flit.nvim](https://github.com/ggandor/flit.nvim)

View File

@ -0,0 +1,671 @@
*flash.nvim.txt* For Neovim >= 0.8.0 Last change: 2024 May 14
==============================================================================
Table of Contents *flash.nvim-table-of-contents*
1. flash.nvim |flash.nvim-flash.nvim|
- Features |flash.nvim-flash.nvim-features|
- Requirements |flash.nvim-flash.nvim-requirements|
- Installation |flash.nvim-flash.nvim-installation|
- Configuration |flash.nvim-flash.nvim-configuration|
- Usage |flash.nvim-flash.nvim-usage|
- API |flash.nvim-flash.nvim-api|
- Examples |flash.nvim-flash.nvim-examples|
- Highlights |flash.nvim-flash.nvim-highlights|
- Alternatives |flash.nvim-flash.nvim-alternatives|
==============================================================================
1. flash.nvim *flash.nvim-flash.nvim*
`flash.nvim` lets you navigate your code with search labels, enhanced character
motions, and Treesitter integration.
Search IntegrationStandalone Jumpf, t, F, TTreesitter
FEATURES *flash.nvim-flash.nvim-features*
- **Search Integration**integrate **flash.nvim** with your regular
search using `/` or `?`. Labels appear next to the matches,
allowing you to quickly jump to any location. Labels are
guaranteed not to exist as a continuation of the search pattern.
- **type as many characters as you want** before using a jump label.
- **Enhanced f, t, F, T motions**
- **Treesitter Integration**all parents of the Treesitter node
under your cursor are highlighted with a label for quick selection
of a specific Treesitter node.
- **Jump Mode**a standalone jumping mode similar to search
- **Search Modes**`exact`, `search` (regex), and `fuzzy` search modes
- **Multi Window** jumping
- **Remote Actions**perform motions in remote locations
- **dot-repeatable** jumps
- **highly extensible**check the examples <https://github.com/folke/flash.nvim#-examples>
REQUIREMENTS *flash.nvim-flash.nvim-requirements*
- Neovim >= **0.8.0** (needs to be built with **LuaJIT**)
INSTALLATION *flash.nvim-flash.nvim-installation*
Install the plugin with your preferred package manager:
lazy.nvim <https://github.com/folke/lazy.nvim>
>lua
{
"folke/flash.nvim",
event = "VeryLazy",
---@type Flash.Config
opts = {},
-- stylua: ignore
keys = {
{ "s", mode = { "n", "x", "o" }, function() require("flash").jump() end, desc = "Flash" },
{ "S", mode = { "n", "x", "o" }, function() require("flash").treesitter() end, desc = "Flash Treesitter" },
{ "r", mode = "o", function() require("flash").remote() end, desc = "Remote Flash" },
{ "R", mode = { "o", "x" }, function() require("flash").treesitter_search() end, desc = "Treesitter Search" },
{ "<c-s>", mode = { "c" }, function() require("flash").toggle() end, desc = "Toggle Flash Search" },
},
}
<
When creating the keymaps manually either use a lua function like `function()
require("flash").jump() end` as the **rhs**, or a string like `<cmd>lua
require("flash").jump()<cr>`. **DO NOT** use `:lua`, since that will break
**dot-repeat**
CONFIGURATION *flash.nvim-flash.nvim-configuration*
**flash.nvim** is highly configurable. Please refer to the default settings
below.
Default Settings ~
>lua
{
-- labels = "abcdefghijklmnopqrstuvwxyz",
labels = "asdfghjklqwertyuiopzxcvbnm",
search = {
-- search/jump in all windows
multi_window = true,
-- search direction
forward = true,
-- when `false`, find only matches in the given direction
wrap = true,
---@type Flash.Pattern.Mode
-- Each mode will take ignorecase and smartcase into account.
-- * exact: exact match
-- * search: regular search
-- * fuzzy: fuzzy search
-- * fun(str): custom function that returns a pattern
-- For example, to only match at the beginning of a word:
-- mode = function(str)
-- return "\\<" .. str
-- end,
mode = "exact",
-- behave like `incsearch`
incremental = false,
-- Excluded filetypes and custom window filters
---@type (string|fun(win:window))[]
exclude = {
"notify",
"cmp_menu",
"noice",
"flash_prompt",
function(win)
-- exclude non-focusable windows
return not vim.api.nvim_win_get_config(win).focusable
end,
},
-- Optional trigger character that needs to be typed before
-- a jump label can be used. It's NOT recommended to set this,
-- unless you know what you're doing
trigger = "",
-- max pattern length. If the pattern length is equal to this
-- labels will no longer be skipped. When it exceeds this length
-- it will either end in a jump or terminate the search
max_length = false, ---@type number|false
},
jump = {
-- save location in the jumplist
jumplist = true,
-- jump position
pos = "start", ---@type "start" | "end" | "range"
-- add pattern to search history
history = false,
-- add pattern to search register
register = false,
-- clear highlight after jump
nohlsearch = false,
-- automatically jump when there is only one match
autojump = false,
-- You can force inclusive/exclusive jumps by setting the
-- `inclusive` option. By default it will be automatically
-- set based on the mode.
inclusive = nil, ---@type boolean?
-- jump position offset. Not used for range jumps.
-- 0: default
-- 1: when pos == "end" and pos < current position
offset = nil, ---@type number
},
label = {
-- allow uppercase labels
uppercase = true,
-- add any labels with the correct case here, that you want to exclude
exclude = "",
-- add a label for the first match in the current window.
-- you can always jump to the first match with `<CR>`
current = true,
-- show the label after the match
after = true, ---@type boolean|number[]
-- show the label before the match
before = false, ---@type boolean|number[]
-- position of the label extmark
style = "overlay", ---@type "eol" | "overlay" | "right_align" | "inline"
-- flash tries to re-use labels that were already assigned to a position,
-- when typing more characters. By default only lower-case labels are re-used.
reuse = "lowercase", ---@type "lowercase" | "all" | "none"
-- for the current window, label targets closer to the cursor first
distance = true,
-- minimum pattern length to show labels
-- Ignored for custom labelers.
min_pattern_length = 0,
-- Enable this to use rainbow colors to highlight labels
-- Can be useful for visualizing Treesitter ranges.
rainbow = {
enabled = false,
-- number between 1 and 9
shade = 5,
},
-- With `format`, you can change how the label is rendered.
-- Should return a list of `[text, highlight]` tuples.
---@class Flash.Format
---@field state Flash.State
---@field match Flash.Match
---@field hl_group string
---@field after boolean
---@type fun(opts:Flash.Format): string[][]
format = function(opts)
return { { opts.match.label, opts.hl_group } }
end,
},
highlight = {
-- show a backdrop with hl FlashBackdrop
backdrop = true,
-- Highlight the search matches
matches = true,
-- extmark priority
priority = 5000,
groups = {
match = "FlashMatch",
current = "FlashCurrent",
backdrop = "FlashBackdrop",
label = "FlashLabel",
},
},
-- action to perform when picking a label.
-- defaults to the jumping logic depending on the mode.
---@type fun(match:Flash.Match, state:Flash.State)|nil
action = nil,
-- initial pattern to use when opening flash
pattern = "",
-- When `true`, flash will try to continue the last search
continue = false,
-- Set config to a function to dynamically change the config
config = nil, ---@type fun(opts:Flash.Config)|nil
-- You can override the default options for a specific mode.
-- Use it with `require("flash").jump({mode = "forward"})`
---@type table<string, Flash.Config>
modes = {
-- options used when flash is activated through
-- a regular search with `/` or `?`
search = {
-- when `true`, flash will be activated during regular search by default.
-- You can always toggle when searching with `require("flash").toggle()`
enabled = false,
highlight = { backdrop = false },
jump = { history = true, register = true, nohlsearch = true },
search = {
-- `forward` will be automatically set to the search direction
-- `mode` is always set to `search`
-- `incremental` is set to `true` when `incsearch` is enabled
},
},
-- options used when flash is activated through
-- `f`, `F`, `t`, `T`, `;` and `,` motions
char = {
enabled = true,
-- dynamic configuration for ftFT motions
config = function(opts)
-- autohide flash when in operator-pending mode
opts.autohide = opts.autohide or (vim.fn.mode(true):find("no") and vim.v.operator == "y")
-- disable jump labels when not enabled, when using a count,
-- or when recording/executing registers
opts.jump_labels = opts.jump_labels
and vim.v.count == 0
and vim.fn.reg_executing() == ""
and vim.fn.reg_recording() == ""
-- Show jump labels only in operator-pending mode
-- opts.jump_labels = vim.v.count == 0 and vim.fn.mode(true):find("o")
end,
-- hide after jump when not using jump labels
autohide = false,
-- show jump labels
jump_labels = false,
-- set to `false` to use the current line only
multi_line = true,
-- When using jump labels, don't use these keys
-- This allows using those keys directly after the motion
label = { exclude = "hjkliardc" },
-- by default all keymaps are enabled, but you can disable some of them,
-- by removing them from the list.
-- If you rather use another key, you can map them
-- to something else, e.g., { [";"] = "L", [","] = H }
keys = { "f", "F", "t", "T", ";", "," },
---@alias Flash.CharActions table<string, "next" | "prev" | "right" | "left">
-- The direction for `prev` and `next` is determined by the motion.
-- `left` and `right` are always left and right.
char_actions = function(motion)
return {
[";"] = "next", -- set to `right` to always go right
[","] = "prev", -- set to `left` to always go left
-- clever-f style
[motion:lower()] = "next",
[motion:upper()] = "prev",
-- jump2d style: same case goes next, opposite case goes prev
-- [motion] = "next",
-- [motion:match("%l") and motion:upper() or motion:lower()] = "prev",
}
end,
search = { wrap = false },
highlight = { backdrop = true },
jump = { register = false },
},
-- options used for treesitter selections
-- `require("flash").treesitter()`
treesitter = {
labels = "abcdefghijklmnopqrstuvwxyz",
jump = { pos = "range" },
search = { incremental = false },
label = { before = true, after = true, style = "inline" },
highlight = {
backdrop = false,
matches = false,
},
},
treesitter_search = {
jump = { pos = "range" },
search = { multi_window = true, wrap = true, incremental = false },
remote_op = { restore = true },
label = { before = true, after = true, style = "inline" },
},
-- options used for remote flash
remote = {
remote_op = { restore = true, motion = true },
},
},
-- options for the floating window that shows the prompt,
-- for regular jumps
prompt = {
enabled = true,
prefix = { { "⚡", "FlashPromptIcon" } },
win_config = {
relative = "editor",
width = 1, -- when <=1 it's a percentage of the editor width
height = 1,
row = -1, -- when negative it's an offset from the bottom
col = 0, -- when negative it's an offset from the right
zindex = 1000,
},
},
-- options for remote operator pending mode
remote_op = {
-- restore window views and cursor position
-- after doing a remote operation
restore = false,
-- For `jump.pos = "range"`, this setting is ignored.
-- `true`: always enter a new motion when doing a remote operation
-- `false`: use the window's cursor position and jump target
-- `nil`: act as `true` for remote windows, `false` for the current window
motion = false,
},
}
<
USAGE *flash.nvim-flash.nvim-usage*
- **Treesitter**`require("flash").treesitter(opts?)` opens **flash** in
**Treesitter** mode
- use a jump label, or use `;` and `,` to increase/decrease the selection
- **regular search**search as you normally do, but enhanced with jump labels. You
need to set `opts.modes.search.enabled = true`, or toggle it with
`require("flash").toggle()`
- `f`, `t`, `F`, `T` motions:
- After typing `f{char}` or `F{char},` you can repeat the motion with `f`
or go to the previous match with `F` to undo a jump.
- Similarly, after typing `t{char}` or `T{char},` you can repeat the motion
with `t` or go to the previous match with `T`.
- You can also go to the next match with `;` or previous match with `,`
- Any highlights clear automatically when moving, changing buffers,
or pressing `<esc>`.
- **toggle Search**`require("flash").toggle(boolean?)`
- toggles **flash** on or off while using regular search
- **Treesitter Search**`require("flash").treesitter_search(opts?)` opens
**flash** in **Treesitter Search** mode
- combination of **Treesitter** and **Search** modes
- do something like `yR`
- you can now start typing a search pattern.
- arround your matches, all the surrounding Treesitter nodes will be labeled.
- select a label to perform the operator on the new selection
- **remote**`require("flash").remote(opts?)` opens **flash** in **remote** mode
- equivalent to:
>lua
require("flash").jump({
remote_op = {
restore = true,
motion = true,
},
})
<
- this is only useful in operator pending mode.
- For example, press `yr` to start yanking and open flash
- select a label to set the cursor position
- perform any motion, like `iw` or even start flash Treesitter with `S`
- the yank will be performed on the new selection
- youll be back in the original window / position
- You can also configure the `remote_op` options by default, so that `ys`,
behaves like `yr` for remote operations
>lua
require("flash").jump({
remote_op = {
restore = true,
motion = nil,
},
})
<
- **jump**`require("flash").jump(opts?)` opens **flash** with the given options
- type any number of characters before typing a jump label
- **VS Code**some functionality is changed/disabled when running **flash** in
**VS Code**
- `prompt`is disabled
- `highlights` are set to different defaults that will actually work in VS Code
API *flash.nvim-flash.nvim-api*
The options for `require("flash").jump(opts?)`, are the same as those in the
config section, but can additionally have the following fields:
- `matcher`a custom function that generates matches for a given window
- `labeler`a custom function to label matches
You can also add labels in the `matcher` function and then set `labeler` to an
empty function `labeler = function() end`
Type Definitions ~
>typescript
type FlashMatcher = (win: number, state: FlashState) => FlashMatch[];
type FlashLabeler = (matches: FlashMatch[], state: FlashState) => void;
interface FlashMatch {
win: number;
pos: [number, number]; // (1,0)-indexed
end_pos: [number, number]; // (1,0)-indexed
label?: string | false; // set to false to never show a label for this match
highlight?: boolean; // override opts.highlight.matches for this match
}
// Check the code for the full definition
// of Flash.State at `lua/flash/state.lua`
type FlashState = {};
<
EXAMPLES *flash.nvim-flash.nvim-examples*
Forward search only ~
>lua
require("flash").jump({
search = { forward = true, wrap = false, multi_window = false },
})
<
Backward search only ~
>lua
require("flash").jump({
search = { forward = false, wrap = false, multi_window = false },
})
<
Show diagnostics at target, without changing cursor position ~
>lua
require("flash").jump({
action = function(match, state)
vim.api.nvim_win_call(match.win, function()
vim.api.nvim_win_set_cursor(match.win, match.pos)
vim.diagnostic.open_float()
end)
state:restore()
end,
})
-- More advanced example that also highlights diagnostics:
require("flash").jump({
matcher = function(win)
---@param diag Diagnostic
return vim.tbl_map(function(diag)
return {
pos = { diag.lnum + 1, diag.col },
end_pos = { diag.end_lnum + 1, diag.end_col - 1 },
}
end, vim.diagnostic.get(vim.api.nvim_win_get_buf(win)))
end,
action = function(match, state)
vim.api.nvim_win_call(match.win, function()
vim.api.nvim_win_set_cursor(match.win, match.pos)
vim.diagnostic.open_float()
end)
state:restore()
end,
})
<
Match beginning of words only ~
>lua
require("flash").jump({
search = {
mode = function(str)
return "\\<" .. str
end,
},
})
<
Initialize flash with the word under the cursor ~
>lua
require("flash").jump({
pattern = vim.fn.expand("<cword>"),
})
<
Jump to a line ~
>lua
require("flash").jump({
search = { mode = "search", max_length = 0 },
label = { after = { 0, 0 } },
pattern = "^"
})
<
Select any word ~
>lua
require("flash").jump({
pattern = ".", -- initialize pattern with any char
search = {
mode = function(pattern)
-- remove leading dot
if pattern:sub(1, 1) == "." then
pattern = pattern:sub(2)
end
-- return word pattern and proper skip pattern
return ([[\<%s\w*\>]]):format(pattern), ([[\<%s]]):format(pattern)
end,
},
-- select the range
jump = { pos = "range" },
})
<
f, t, F, T with labels ~
Use the options below:
>lua
{
modes = {
char = {
jump_labels = true
}
}
}
<
Telescope integration ~
This will allow you to use `s` in normal mode and `<c-s>` in insert mode, to
jump to a label in Telescope results.
>lua
{
"nvim-telescope/telescope.nvim",
optional = true,
opts = function(_, opts)
local function flash(prompt_bufnr)
require("flash").jump({
pattern = "^",
label = { after = { 0, 0 } },
search = {
mode = "search",
exclude = {
function(win)
return vim.bo[vim.api.nvim_win_get_buf(win)].filetype ~= "TelescopeResults"
end,
},
},
action = function(match)
local picker = require("telescope.actions.state").get_current_picker(prompt_bufnr)
picker:set_selection(match.pos[1] - 1)
end,
})
end
opts.defaults = vim.tbl_deep_extend("force", opts.defaults or {}, {
mappings = {
n = { s = flash },
i = { ["<c-s>"] = flash },
},
})
end,
}
<
Continue last search ~
>lua
require("flash").jump({continue = true})
<
2-char jump, similar to
mini.jump2d
or
HopWord (hop.nvim)
~
>lua
local Flash = require("flash")
---@param opts Flash.Format
local function format(opts)
-- always show first and second label
return {
{ opts.match.label1, "FlashMatch" },
{ opts.match.label2, "FlashLabel" },
}
end
Flash.jump({
search = { mode = "search" },
label = { after = false, before = { 0, 0 }, uppercase = false, format = format },
pattern = [[\<]],
action = function(match, state)
state:hide()
Flash.jump({
search = { max_length = 0 },
highlight = { matches = false },
label = { format = format },
matcher = function(win)
-- limit matches to the current label
return vim.tbl_filter(function(m)
return m.label == match.label and m.win == win
end, state.results)
end,
labeler = function(matches)
for _, m in ipairs(matches) do
m.label = m.label2 -- use the second label
end
end,
})
end,
labeler = function(matches, state)
local labels = state:labels()
for m, match in ipairs(matches) do
match.label1 = labels[math.floor((m - 1) / #labels) + 1]
match.label2 = labels[(m - 1) % #labels + 1]
match.label = match.label1
end
end,
})
<
HIGHLIGHTS *flash.nvim-flash.nvim-highlights*
Group Default Description
----------------- ------------ ----------------
FlashBackdrop Comment backdrop
FlashMatch Search search matches
FlashCurrent IncSearch current match
FlashLabel Substitute jump label
FlashPrompt MsgArea prompt
FlashPromptIcon Special prompt icon
FlashCursor Cursor cursor
ALTERNATIVES *flash.nvim-flash.nvim-alternatives*
- leap.nvim <https://github.com/ggandor/leap.nvim>
- lightspeed.nvim <https://github.com/ggandor/lightspeed.nvim>
- vim-sneak <https://github.com/justinmk/vim-sneak>
- mini.jump <https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-jump.md>
- mini.jump2d <https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-jump2d.md>
- hop.nvim <https://github.com/phaazon/hop.nvim>
- pounce.nvim <https://github.com/rlane/pounce.nvim>
- sj.nvim <https://github.com/woosaaahh/sj.nvim>
- nvim-treehopper <https://github.com/mfussenegger/nvim-treehopper>
- flit.nvim <https://github.com/ggandor/flit.nvim>
Generated by panvimdoc <https://github.com/kdheepak/panvimdoc>
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,149 @@
local Pattern = require("flash.search.pattern")
local Pos = require("flash.search.pos")
local Util = require("flash.util")
---@class Flash.State.Window
---@field win number
---@field buf number
---@field topline number
---@field botline number
---@field changedtick number
---@class Flash.Cache
---@field state Flash.State
---@field pattern Flash.Pattern
---@field wins Flash.State.Window[]
local M = {}
M.__index = M
---@type table<Flash.State.Window, {matches: Flash.Match[]}>
M.cache = setmetatable({}, { __mode = "k" })
---@param state Flash.State
function M.new(state)
local self = setmetatable({}, M)
self.state = state
self.pattern = Pattern.new("", state.opts.search.mode, state.opts.search.trigger)
self.wins = {}
return self
end
---@return boolean dirty Returns true when dirty
function M:update()
local dirty = false
if self.pattern ~= self.state.pattern then
self.pattern = self.state.pattern:clone()
dirty = true
M.cache = {}
end
local win = vim.api.nvim_get_current_win()
if self.state.win ~= win then
self.state.win = win
self.state.pos = Pos(win)
self.state.restore_windows = Util.save_layout()
M.cache = {}
dirty = true
end
self:_update_wins()
for _, w in ipairs(self.state.wins) do
if self:_dirty(w) then
dirty = true
end
end
return dirty
end
---@param win window
function M:get_state(win)
local window = self:get(win)
if not window then
return
end
if M.cache[window] then
return M.cache[window]
end
local from = Pos({ window.topline, 0 })
local to = Pos({ window.botline + 1, 0 })
if not self.state.opts.search.wrap and win == self.state.win then
if self.state.opts.search.forward then
from = self.state.pos
else
to = self.state.pos
end
end
local matcher = self.state:get_matcher(win)
if matcher.update then
matcher:update()
end
M.cache[window] = {
matches = matcher:get({ from = from, to = to }),
}
return M.cache[window]
end
---@param win window
---@return Flash.State.Window
function M:get(win)
return self.wins[win]
end
function M:_update_wins()
-- prioritize current window
self.state.wins = { self.state.win }
if self.state.opts.search.multi_window then
local keep_current = false
---@param w window
self.state.wins = vim.tbl_filter(function(w)
local buf = vim.api.nvim_win_get_buf(w)
local ft = vim.bo[buf].filetype
for _, exclude in ipairs(self.state.opts.search.exclude) do
if type(exclude) == "string" and exclude == ft then
return false
elseif type(exclude) == "function" and exclude(w) then
return false
end
end
if w == self.state.win then
keep_current = true
return false
end
return true
end, vim.api.nvim_tabpage_list_wins(0))
if keep_current then
table.insert(self.state.wins, 1, self.state.win)
end
end
end
---@param win window
function M:_dirty(win)
local info = vim.fn.getwininfo(win)[1]
local buf = vim.api.nvim_win_get_buf(win)
---@type Flash.State.Window
local state = {
win = win,
buf = buf,
cursor = vim.api.nvim_win_get_cursor(win),
topline = info.topline,
botline = info.botline,
changedtick = vim.b[buf].changedtick,
}
if not vim.deep_equal(state, self.wins[win]) then
self.wins[win] = state
return true
end
end
return M

View File

@ -0,0 +1,36 @@
local Repeat = require("flash.repeat")
---@class Flash.Commands
local M = {}
---@param opts? Flash.State.Config
function M.jump(opts)
local state = Repeat.get_state("jump", opts)
state:loop()
return state
end
---@param opts? Flash.State.Config
function M.treesitter(opts)
return require("flash.plugins.treesitter").jump(opts)
end
---@param opts? Flash.State.Config
function M.treesitter_search(opts)
return require("flash.plugins.treesitter").search(opts)
end
---@param opts? Flash.State.Config
function M.remote(opts)
local Config = require("flash.config")
opts = Config.get({ mode = "remote" }, opts)
return M.jump(opts)
end
---@param enabled? boolean
function M.toggle(enabled)
local Search = require("flash.plugins.search")
return Search.toggle(enabled)
end
return M

View File

@ -0,0 +1,345 @@
---@type Flash.Config
local M = {}
---@class Flash.Config
---@field mode? string
---@field enabled? boolean
---@field ns? string
---@field config? fun(opts:Flash.Config)
local defaults = {
-- labels = "abcdefghijklmnopqrstuvwxyz",
labels = "asdfghjklqwertyuiopzxcvbnm",
search = {
-- search/jump in all windows
multi_window = true,
-- search direction
forward = true,
-- when `false`, find only matches in the given direction
wrap = true,
---@type Flash.Pattern.Mode
-- Each mode will take ignorecase and smartcase into account.
-- * exact: exact match
-- * search: regular search
-- * fuzzy: fuzzy search
-- * fun(str): custom function that returns a pattern
-- For example, to only match at the beginning of a word:
-- mode = function(str)
-- return "\\<" .. str
-- end,
mode = "exact",
-- behave like `incsearch`
incremental = false,
-- Excluded filetypes and custom window filters
---@type (string|fun(win:window))[]
exclude = {
"notify",
"cmp_menu",
"noice",
"flash_prompt",
function(win)
-- exclude non-focusable windows
return not vim.api.nvim_win_get_config(win).focusable
end,
},
-- Optional trigger character that needs to be typed before
-- a jump label can be used. It's NOT recommended to set this,
-- unless you know what you're doing
trigger = "",
-- max pattern length. If the pattern length is equal to this
-- labels will no longer be skipped. When it exceeds this length
-- it will either end in a jump or terminate the search
max_length = false, ---@type number|false
},
jump = {
-- save location in the jumplist
jumplist = true,
-- jump position
pos = "start", ---@type "start" | "end" | "range"
-- add pattern to search history
history = false,
-- add pattern to search register
register = false,
-- clear highlight after jump
nohlsearch = false,
-- automatically jump when there is only one match
autojump = false,
-- You can force inclusive/exclusive jumps by setting the
-- `inclusive` option. By default it will be automatically
-- set based on the mode.
inclusive = nil, ---@type boolean?
-- jump position offset. Not used for range jumps.
-- 0: default
-- 1: when pos == "end" and pos < current position
offset = nil, ---@type number
},
label = {
-- allow uppercase labels
uppercase = true,
-- add any labels with the correct case here, that you want to exclude
exclude = "",
-- add a label for the first match in the current window.
-- you can always jump to the first match with `<CR>`
current = true,
-- show the label after the match
after = true, ---@type boolean|number[]
-- show the label before the match
before = false, ---@type boolean|number[]
-- position of the label extmark
style = "overlay", ---@type "eol" | "overlay" | "right_align" | "inline"
-- flash tries to re-use labels that were already assigned to a position,
-- when typing more characters. By default only lower-case labels are re-used.
reuse = "lowercase", ---@type "lowercase" | "all" | "none"
-- for the current window, label targets closer to the cursor first
distance = true,
-- minimum pattern length to show labels
-- Ignored for custom labelers.
min_pattern_length = 0,
-- Enable this to use rainbow colors to highlight labels
-- Can be useful for visualizing Treesitter ranges.
rainbow = {
enabled = false,
-- number between 1 and 9
shade = 5,
},
-- With `format`, you can change how the label is rendered.
-- Should return a list of `[text, highlight]` tuples.
---@class Flash.Format
---@field state Flash.State
---@field match Flash.Match
---@field hl_group string
---@field after boolean
---@type fun(opts:Flash.Format): string[][]
format = function(opts)
return { { opts.match.label, opts.hl_group } }
end,
},
highlight = {
-- show a backdrop with hl FlashBackdrop
backdrop = true,
-- Highlight the search matches
matches = true,
-- extmark priority
priority = 5000,
groups = {
match = "FlashMatch",
current = "FlashCurrent",
backdrop = "FlashBackdrop",
label = "FlashLabel",
},
},
-- action to perform when picking a label.
-- defaults to the jumping logic depending on the mode.
---@type fun(match:Flash.Match, state:Flash.State)|nil
action = nil,
-- initial pattern to use when opening flash
pattern = "",
-- When `true`, flash will try to continue the last search
continue = false,
-- Set config to a function to dynamically change the config
config = nil, ---@type fun(opts:Flash.Config)|nil
-- You can override the default options for a specific mode.
-- Use it with `require("flash").jump({mode = "forward"})`
---@type table<string, Flash.Config>
modes = {
-- options used when flash is activated through
-- a regular search with `/` or `?`
search = {
-- when `true`, flash will be activated during regular search by default.
-- You can always toggle when searching with `require("flash").toggle()`
enabled = false,
highlight = { backdrop = false },
jump = { history = true, register = true, nohlsearch = true },
search = {
-- `forward` will be automatically set to the search direction
-- `mode` is always set to `search`
-- `incremental` is set to `true` when `incsearch` is enabled
},
},
-- options used when flash is activated through
-- `f`, `F`, `t`, `T`, `;` and `,` motions
char = {
enabled = true,
-- dynamic configuration for ftFT motions
config = function(opts)
-- autohide flash when in operator-pending mode
opts.autohide = opts.autohide or (vim.fn.mode(true):find("no") and vim.v.operator == "y")
-- disable jump labels when not enabled, when using a count,
-- or when recording/executing registers
opts.jump_labels = opts.jump_labels
and vim.v.count == 0
and vim.fn.reg_executing() == ""
and vim.fn.reg_recording() == ""
-- Show jump labels only in operator-pending mode
-- opts.jump_labels = vim.v.count == 0 and vim.fn.mode(true):find("o")
end,
-- hide after jump when not using jump labels
autohide = false,
-- show jump labels
jump_labels = false,
-- set to `false` to use the current line only
multi_line = true,
-- When using jump labels, don't use these keys
-- This allows using those keys directly after the motion
label = { exclude = "hjkliardc" },
-- by default all keymaps are enabled, but you can disable some of them,
-- by removing them from the list.
-- If you rather use another key, you can map them
-- to something else, e.g., { [";"] = "L", [","] = H }
keys = { "f", "F", "t", "T", ";", "," },
---@alias Flash.CharActions table<string, "next" | "prev" | "right" | "left">
-- The direction for `prev` and `next` is determined by the motion.
-- `left` and `right` are always left and right.
char_actions = function(motion)
return {
[";"] = "next", -- set to `right` to always go right
[","] = "prev", -- set to `left` to always go left
-- clever-f style
[motion:lower()] = "next",
[motion:upper()] = "prev",
-- jump2d style: same case goes next, opposite case goes prev
-- [motion] = "next",
-- [motion:match("%l") and motion:upper() or motion:lower()] = "prev",
}
end,
search = { wrap = false },
highlight = { backdrop = true },
jump = { register = false },
},
-- options used for treesitter selections
-- `require("flash").treesitter()`
treesitter = {
labels = "abcdefghijklmnopqrstuvwxyz",
jump = { pos = "range" },
search = { incremental = false },
label = { before = true, after = true, style = "inline" },
highlight = {
backdrop = false,
matches = false,
},
},
treesitter_search = {
jump = { pos = "range" },
search = { multi_window = true, wrap = true, incremental = false },
remote_op = { restore = true },
label = { before = true, after = true, style = "inline" },
},
-- options used for remote flash
remote = {
remote_op = { restore = true, motion = true },
},
},
-- options for the floating window that shows the prompt,
-- for regular jumps
prompt = {
enabled = true,
prefix = { { "", "FlashPromptIcon" } },
win_config = {
relative = "editor",
width = 1, -- when <=1 it's a percentage of the editor width
height = 1,
row = -1, -- when negative it's an offset from the bottom
col = 0, -- when negative it's an offset from the right
zindex = 1000,
},
},
-- options for remote operator pending mode
remote_op = {
-- restore window views and cursor position
-- after doing a remote operation
restore = false,
-- For `jump.pos = "range"`, this setting is ignored.
-- `true`: always enter a new motion when doing a remote operation
-- `false`: use the window's cursor position and jump target
-- `nil`: act as `true` for remote windows, `false` for the current window
motion = false,
},
}
---@type Flash.Config
local options
---@param opts? Flash.Config
function M.setup(opts)
opts = opts or {}
opts.mode = nil
options = {}
options = M.get(opts)
require("flash.plugins.search").setup()
if options.modes.char.enabled then
require("flash.plugins.char").setup()
end
end
---@param ... Flash.Config|Flash.State.Config|nil
---@return Flash.State.Config
function M.get(...)
if options == nil then
M.setup()
end
---@type Flash.Config[]
local all = { {}, defaults, options or {} }
---@type table<string, boolean>
local modes = {}
for i = 1, select("#", ...) do
---@type Flash.Config?
local opts = select(i, ...)
if type(opts) == "string" then
opts = options.modes[opts]
end
if opts then
table.insert(all, opts)
local idx = #all
while opts.mode and not modes[opts.mode] do
modes[opts.mode or ""] = true
opts = options.modes[opts.mode] or {}
table.insert(all, idx, opts)
end
end
end
-- backward compatibility
for _, o in ipairs(all) do
if o.highlight and o.highlight.label then
o.label = vim.tbl_deep_extend("force", o.label or {}, o.highlight.label)
---@diagnostic disable-next-line: no-unknown
o.highlight.label = nil
vim.notify_once(
"flash: `opts.highlight.label` is deprecated, use `opts.label` instead",
vim.log.levels.WARN
)
end
for _, field in ipairs({ "autohide", "jump_labels" }) do
if type(o[field]) == "function" then
local motion = require("flash.plugins.char").motion
---@diagnostic disable-next-line: no-unknown
o[field] = o[field](motion)
end
end
end
local ret = vim.tbl_deep_extend("force", unpack(all))
---@cast ret Flash.State.Config
if type(ret.config) == "function" then
ret.config(ret)
end
if vim.g.vscode then
ret.prompt.enabled = false
end
return ret
end
return setmetatable(M, {
__index = function(_, key)
if options == nil then
M.setup()
end
return options[key]
end,
})

View File

@ -0,0 +1,32 @@
local Docs = require("lazy.docs")
local M = {}
function M.update()
local config = Docs.extract("lua/flash/config.lua", "\nlocal defaults = ({.-\n})")
config = config:gsub("%s*debug = false.\n", "\n")
Docs.save({
config = config,
setup = Docs.extract("lua/flash/docs.lua", "function M%.suggested%(%)\n%s*return (.-)\nend"),
})
end
function M.suggested()
return {
"folke/flash.nvim",
event = "VeryLazy",
---@type Flash.Config
opts = {},
-- stylua: ignore
keys = {
{ "s", mode = { "n", "x", "o" }, function() require("flash").jump() end, desc = "Flash" },
{ "S", mode = { "n", "x", "o" }, function() require("flash").treesitter() end, desc = "Flash Treesitter" },
{ "r", mode = "o", function() require("flash").remote() end, desc = "Remote Flash" },
{ "R", mode = { "o", "x" }, function() require("flash").treesitter_search() end, desc = "Treesitter Search" },
{ "<c-s>", mode = { "c" }, function() require("flash").toggle() end, desc = "Toggle Flash Search" },
},
}
end
M.update()
return M

View File

@ -0,0 +1,68 @@
local Pos = require("flash.search.pos")
local M = {}
---@type ffi.namespace*
local C
local incsearch_state = {}
local function _ffi()
if not C then
local ffi = require("ffi")
ffi.cdef([[
int search_match_endcol;
int no_mapping;
unsigned int search_match_lines;
void setcursor_mayforce(bool force);
]])
C = ffi.C
end
return C
end
---@private
---@param from Pos
function M.get_end_pos(from)
_ffi()
local ret = Pos({
from[1] + C.search_match_lines,
math.max(0, C.search_match_endcol - 1),
})
local line = vim.api.nvim_buf_get_lines(0, ret[1] - 1, ret[1], false)[1]
local char_idx = vim.fn.charidx(line, ret[2])
ret[2] = vim.fn.byteidx(line, char_idx)
return ret
end
function M.save_incsearch_state()
_ffi()
incsearch_state = {
match_endcol = C.search_match_endcol,
match_lines = C.search_match_lines,
}
end
function M.mappings_enabled()
_ffi()
return C.no_mapping == 0
end
function M.setcursor(force)
if vim.api.nvim__redraw then
vim.api.nvim__redraw({ cursor = true })
else
if force == nil then
force = false
end
_ffi()
return C.setcursor_mayforce(force)
end
end
function M.restore_incsearch_state()
_ffi()
C.search_match_endcol = incsearch_state.match_endcol
C.search_match_lines = incsearch_state.match_lines
end
return M

View File

@ -0,0 +1,216 @@
local M = {}
function M.clear(ns)
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
vim.api.nvim_buf_clear_namespace(buf, ns, 0, -1)
end
end
function M.setup()
if vim.g.vscode then
local hls = {
FlashBackdrop = { fg = "#545c7e" },
FlashCurrent = { bg = "#ff966c", fg = "#1b1d2b" },
FlashLabel = { bg = "#ff007c", bold = true, fg = "#c8d3f5" },
FlashMatch = { bg = "#3e68d7", fg = "#c8d3f5" },
FlashCursor = { reverse = true },
}
for hl_group, hl in pairs(hls) do
hl.default = true
vim.api.nvim_set_hl(0, hl_group, hl)
end
else
local links = {
FlashBackdrop = "Comment",
FlashMatch = "Search",
FlashCurrent = "IncSearch",
FlashLabel = "Substitute",
FlashPrompt = "MsgArea",
FlashPromptIcon = "Special",
FlashCursor = "Cursor",
}
for hl_group, link in pairs(links) do
vim.api.nvim_set_hl(0, hl_group, { link = link, default = true })
end
end
end
M.setup()
---@param state Flash.State
function M.backdrop(state)
for _, win in ipairs(state.wins) do
local info = vim.fn.getwininfo(win)[1]
local buf = vim.api.nvim_win_get_buf(win)
local from = { info.topline, 0 }
local to = { info.botline + 1, 0 }
if state.win == win and not state.opts.search.wrap then
if state.opts.search.forward then
from = { state.pos[1], state.pos[2] + 1 }
else
to = state.pos
end
end
-- we need to create a backdrop for each line because of the way
-- extmarks priority rendering works
for line = from[1], to[1] do
vim.api.nvim_buf_set_extmark(buf, state.ns, line - 1, line == from[1] and from[2] or 0, {
hl_group = state.opts.highlight.groups.backdrop,
end_row = line == to[1] and line - 1 or line,
hl_eol = line ~= to[1],
end_col = line == to[1] and to[2] or from[2],
priority = state.opts.highlight.priority,
strict = false,
})
end
end
end
---@param state Flash.State
function M.cursor(state)
for _, win in ipairs(state.wins) do
local cursor = vim.api.nvim_win_get_cursor(win)
local buf = vim.api.nvim_win_get_buf(win)
vim.api.nvim_buf_set_extmark(buf, state.ns, cursor[1] - 1, cursor[2], {
hl_group = "FlashCursor",
end_col = cursor[2] + 1,
priority = state.opts.highlight.priority + 3,
strict = false,
})
end
end
---@param state Flash.State
function M.update(state)
M.clear(state.ns)
if state.opts.highlight.backdrop then
M.backdrop(state)
end
local style = state.opts.label.style
if style == "inline" and vim.fn.has("nvim-0.10.0") == 0 then
style = "overlay"
end
local after = state.opts.label.after
after = after == true and { 0, 1 } or after
---@cast after number[]
local before = state.opts.label.before
before = before == true and { 0, -1 } or before
---@cast before number[]
if style == "inline" and before then
before[2] = before[2] + 1
end
local target = state.target
---@type table<string, {buf: number, row: number, col: number, text:string[][]}>
local extmarks = {}
---@param match Flash.Match
---@param pos number[]
---@param offset number[]
---@param is_after boolean
local function label(match, pos, offset, is_after)
local buf = vim.api.nvim_win_get_buf(match.win)
local cursor = vim.api.nvim_win_get_cursor(match.win)
local pos2 = require("flash.util").offset_pos(buf, pos, offset)
local row, col = pos2[1] - 1, pos2[2]
-- dont show the label if the cursor is on the same position
-- in the same window
-- and the label is not a range
if
cursor[1] == row + 1
and cursor[2] == col
and match.win == state.win
and state.opts.jump.pos ~= "range"
then
return
end
if match.fold then
-- set the row to the fold start
row = match.fold - 1
col = 0
end
local hl_group = state.opts.highlight.groups.label
if state.rainbow then
hl_group = state.rainbow:get(match)
elseif
-- set hl_group to current if the match is the current target
-- and the target is a single character
target
and target.pos[1] == row + 1
and target.pos[2] == col
and target.pos == target.end_pos
then
hl_group = state.opts.highlight.groups.current
end
if match.label == "" then
-- when empty label, highlight the position
vim.api.nvim_buf_set_extmark(buf, state.ns, row, col, {
hl_group = hl_group,
end_row = row,
end_col = col + 1,
strict = false,
priority = state.opts.highlight.priority + 2,
})
else
-- else highlight the label
local key = buf .. ":" .. row .. ":" .. col
extmarks[key] = extmarks[key] or { buf = buf, row = row, col = col, text = {} }
local text = state.opts.label.format({
state = state,
match = match,
hl_group = hl_group,
after = is_after,
})
for i = #text, 1, -1 do
table.insert(extmarks[key].text, 1, text[i])
end
end
end
for _, match in ipairs(state.results) do
local buf = vim.api.nvim_win_get_buf(match.win)
local highlight = state.opts.highlight.matches
if match.highlight ~= nil then
highlight = match.highlight
end
if highlight then
vim.api.nvim_buf_set_extmark(buf, state.ns, match.pos[1] - 1, match.pos[2], {
end_row = match.end_pos[1] - 1,
end_col = match.end_pos[2] + 1,
hl_group = target and match.pos == target.pos and state.opts.highlight.groups.current
or state.opts.highlight.groups.match,
strict = false,
priority = state.opts.highlight.priority + 1,
})
end
end
for _, match in ipairs(state.results) do
if match.label and after then
label(match, match.end_pos, after, true)
end
if match.label and before then
label(match, match.pos, before, false)
end
end
for _, extmark in pairs(extmarks) do
vim.api.nvim_buf_set_extmark(extmark.buf, state.ns, extmark.row, extmark.col, {
virt_text = extmark.text,
virt_text_pos = style,
strict = false,
priority = state.opts.highlight.priority + 2,
})
end
M.cursor(state)
end
return M

View File

@ -0,0 +1,13 @@
---@type Flash.Commands
local M = {}
---@param opts? Flash.Config
function M.setup(opts)
require("flash.config").setup(opts)
end
return setmetatable(M, {
__index = function(_, k)
return require("flash.commands")[k]
end,
})

View File

@ -0,0 +1,253 @@
local Hacks = require("flash.hacks")
local Pos = require("flash.search.pos")
local Util = require("flash.util")
local M = {}
---@param match Flash.Match
---@param state Flash.State
---@return Flash.Match?
function M.jump(match, state)
local register = vim.v.register
-- add to jump list
if state.opts.jump.jumplist then
vim.cmd("normal! m'")
end
local mode = vim.fn.mode(true)
local is_op = mode:sub(1, 2) == "no"
local is_visual = mode:sub(1, 1) == "v"
if is_op and (state.opts.remote_op.motion or match.win ~= vim.api.nvim_get_current_win()) then
-- use our special logic for remote operator pending mode
return M.remote_op(match, state, register)
end
-- change window if needed
if match.win ~= vim.api.nvim_get_current_win() then
if is_visual then
-- cancel visual mode in the current window,
-- to avoid issues with the remote window
vim.cmd("normal! v")
end
vim.api.nvim_set_current_win(match.win)
if is_visual then
-- enable visual mode in the remote window,
-- from its current cursor position
vim.cmd("normal! v")
end
end
M._jump(match, state, { op = is_op })
end
function M.fix_selection()
local selection = vim.go.selection
vim.go.selection = "inclusive"
vim.schedule(function()
vim.go.selection = selection
end)
end
-- Remote operator pending mode.Cancel the operator and
-- re-trigger the operator in the remote window.
---@param match Flash.Match
---@param state Flash.State
---@param register string
---@return Flash.Match?
function M.remote_op(match, state, register)
Util.exit()
-- schedule this so that the active operator is properly cancelled
vim.schedule(function()
local motion = state.opts.remote_op.motion
if motion == nil then
motion = match.win ~= vim.api.nvim_get_current_win()
end
vim.api.nvim_set_current_win(match.win)
-- use a new motion to select the text-object to act on,
-- unless we're jumping to a range
if motion then
if vim.fn.mode() == "v" then
vim.cmd("normal! v")
end
if state.opts.jump.pos == "range" then
vim.api.nvim_win_set_cursor(match.win, match.pos)
vim.cmd("normal! v")
vim.api.nvim_win_set_cursor(match.win, match.end_pos)
else
vim.api.nvim_win_set_cursor(
match.win,
state.opts.jump.pos == "start" and match.pos or match.end_pos
)
end
-- otherwise, use the remote window's cursor position
else
local from = vim.api.nvim_win_get_cursor(match.win)
M._jump(match, state, { op = true })
local to = vim.api.nvim_win_get_cursor(match.win)
-- if a range was selected, use that instead
if vim.fn.mode() == "v" then
vim.cmd("normal! v") -- end the selection
from = vim.api.nvim_buf_get_mark(0, "<")
to = vim.api.nvim_buf_get_mark(0, ">")
end
-- vim.api.nvim_buf_set_mark(0, "[", from[1], from[2], {})
-- vim.api.nvim_buf_set_mark(0, "]", to[1], to[2], {})
-- select the range for the operator
vim.api.nvim_win_set_cursor(0, from)
vim.cmd("normal! v")
vim.api.nvim_win_set_cursor(0, to)
end
---@diagnostic disable-next-line: param-type-mismatch
local opmap = vim.fn.maparg(vim.v.operator, "", false, true) --[[@as any]]
if not vim.tbl_isempty(opmap) then
vim.keymap.del("", vim.v.operator)
end
-- re-trigger the operator
vim.api.nvim_input('"' .. register .. vim.v.operator)
if state.opts.remote_op.restore then
vim.schedule(function()
if not vim.tbl_isempty(opmap) then
vim.fn.mapset(opmap.mode, false, opmap)
end
M.restore_remote(state)
end)
end
end)
end
-- Restore window views after the remote operation ends
---@param state Flash.State
function M.restore_remote(state)
local restore = vim.schedule_wrap(function()
state:restore()
end)
-- wait till getting user input clears
if not Hacks.mappings_enabled() then
return Util.on_done(function()
return Hacks.mappings_enabled()
end, function()
M.restore_remote(state)
end)
-- wait till operator pending mode ends
elseif vim.fn.mode(true):sub(1, 2) == "no" then
return Util.on_done(function()
return vim.fn.mode(true):sub(1, 2) ~= "no"
end, function()
M.restore_remote(state)
end)
-- restore after making edits
elseif vim.fn.mode() == "i" and vim.v.operator == "c" then
vim.api.nvim_create_autocmd("InsertLeave", {
once = true,
callback = restore,
})
else
restore()
end
end
-- Performs the actual jump in the current window,
-- taking operator-pending mode into account.
---@param match Flash.Match
---@param state Flash.State
---@param opts? {op:boolean}
---@return Flash.Match?
function M._jump(match, state, opts)
opts = opts or {}
M.fix_selection()
M.open_folds(match)
-- select range
if state.opts.jump.pos == "range" then
if vim.fn.mode() == "v" then
vim.cmd("normal! v")
end
vim.api.nvim_win_set_cursor(match.win, match.pos)
vim.cmd("normal! v")
vim.api.nvim_win_set_cursor(match.win, match.end_pos)
else
local pos = state.opts.jump.pos == "start" and match.pos or match.end_pos
if opts.op then
-- fix inclusive/exclusive
-- default is exclusive
if state.opts.jump.inclusive ~= false then
vim.cmd("normal! v")
end
end
local current = Pos(match.win)
local offset = state.opts.jump.offset
if not offset and state.opts.jump.pos == "end" and pos < current then
offset = 1
end
pos = Pos(
require("flash.util").offset_pos(vim.api.nvim_win_get_buf(match.win), pos, { 0, offset or 0 })
)
pos[2] = math.max(0, pos[2])
vim.api.nvim_win_set_cursor(match.win, pos)
end
end
---@param match Flash.Match
function M.open_folds(match)
local cursor = vim.api.nvim_win_get_cursor(match.win)
local from = match.pos[1]
local to = match.end_pos[1]
local is_visual = vim.fn.mode(true):find("v")
local opened = false
for line = from, to do
if vim.fn.foldclosed(line) ~= -1 then
vim.api.nvim_win_set_cursor(match.win, { line, 0 })
vim.cmd("normal! zO")
opened = true
end
end
if opened then
vim.api.nvim_win_set_cursor(match.win, cursor)
if is_visual then
vim.cmd("normal! v")
end
end
end
---@param state Flash.State
function M.on_jump(state)
-- fix or restore the search register
local sf = vim.v.searchforward
if state.opts.jump.register then
vim.fn.setreg("/", state.pattern.search)
end
vim.v.searchforward = sf
-- add the real search pattern to the history
if state.opts.jump.history then
vim.fn.histadd("search", state.pattern.search)
end
-- clear the highlight
if state.opts.jump.nohlsearch then
vim.cmd.nohlsearch()
elseif state.opts.jump.register then
-- this will show the search matches again
vim.cmd("set hlsearch")
end
end
return M

View File

@ -0,0 +1,228 @@
---@class Flash.Labeler
---@field state Flash.State
---@field used table<string, string>
---@field labels string[]
local M = {}
M.__index = M
function M.new(state)
local self
self = setmetatable({}, M)
self.state = state
self.used = {}
self:reset()
return self
end
function M:labeler()
return function()
return self:update()
end
end
function M:update()
self:reset()
if #self.state.pattern() < self.state.opts.label.min_pattern_length then
return
end
local matches = self:filter()
for _, match in ipairs(matches) do
self:label(match, true)
end
for _, match in ipairs(matches) do
if not self:label(match) then
break
end
end
end
function M:reset()
local skip = {} ---@type table<string, boolean>
self.labels = {}
for _, l in ipairs(self.state:labels()) do
if not skip[l] then
self.labels[#self.labels + 1] = l
skip[l] = true
end
end
if
not self.state.opts.search.max_length
or #self.state.pattern() < self.state.opts.search.max_length
then
for _, win in pairs(self.state.wins) do
self.labels = self:skip(win, self.labels)
end
end
for _, m in ipairs(self.state.results) do
if m.label ~= false then
m.label = nil
end
end
end
function M:valid(label)
return vim.tbl_contains(self.labels, label)
end
function M:use(label)
self.labels = vim.tbl_filter(function(c)
return c ~= label
end, self.labels)
end
---@param m Flash.Match
---@param used boolean?
function M:label(m, used)
if m.label ~= nil then
return true
end
local pos = m.pos:id(m.win)
local label ---@type string?
if used then
label = self.used[pos]
else
label = self.labels[1]
end
if label and self:valid(label) then
self:use(label)
local reuse = self.state.opts.label.reuse == "all"
or (self.state.opts.label.reuse == "lowercase" and label:lower() == label)
if reuse then
self.used[pos] = label
end
m.label = label
end
return #self.labels > 0
end
function M:filter()
---@type Flash.Match[]
local ret = {}
local target = self.state.target
local from = vim.api.nvim_win_get_cursor(self.state.win)
---@type table<number, boolean>
local folds = {}
-- only label visible matches
for _, match in ipairs(self.state.results) do
-- and don't label the first match in the current window
local skip = (target and match.pos == target.pos)
and not self.state.opts.label.current
and match.win == self.state.win
-- Only label the first match in each fold
if not skip and match.fold then
if folds[match.fold] then
skip = true
else
folds[match.fold] = true
end
end
if not skip then
table.insert(ret, match)
end
end
-- sort by current win, other win, then by distance
table.sort(ret, function(a, b)
local use_distance = self.state.opts.label.distance and a.win == self.state.win
if a.win ~= b.win then
local aw = a.win == self.state.win and 0 or a.win
local bw = b.win == self.state.win and 0 or b.win
return aw < bw
end
if use_distance then
local dfrom = from[1] * vim.go.columns + from[2]
local da = a.pos[1] * vim.go.columns + a.pos[2]
local db = b.pos[1] * vim.go.columns + b.pos[2]
return math.abs(dfrom - da) < math.abs(dfrom - db)
end
if a.pos[1] ~= b.pos[1] then
return a.pos[1] < b.pos[1]
end
return a.pos[2] < b.pos[2]
end)
return ret
end
-- Returns valid labels for the current search pattern
-- in this window.
---@param labels string[]
---@return string[] returns labels to skip or `nil` when all labels should be skipped
function M:skip(win, labels)
local pattern = self.state.pattern.skip
-- skip all labels if the pattern is empty
if pattern == "" then
return {}
end
-- skip all labels if the pattern is invalid
local ok = pcall(vim.regex, pattern)
if not ok then
return {}
end
-- skip all labels if the pattern ends with a backslash
-- except if it's escaped
if pattern:find("\\$") and not pattern:find("\\\\$") then
return {}
end
vim.api.nvim_win_call(win, function()
while #labels > 0 do
-- this is needed, since an uppercase label would trigger smartcase
local label_group = table.concat(labels, "")
if vim.go.ignorecase then
label_group = label_group:lower()
end
local p = "\\%(" .. pattern .. "\\)\\m\\zs[" .. label_group .. "]"
local pos
ok, pos = pcall(vim.fn.searchpos, p, "cnw")
if not ok then
labels = {}
break
end
-- not found, we're done
if pos[1] == 0 then
return
end
local line = vim.api.nvim_buf_get_lines(0, pos[1] - 1, pos[1], false)[1]
local char = vim.fn.strpart(line, pos[2] - 1, 1, true)
local label_count = #labels
labels = vim.tbl_filter(function(c)
-- when ignorecase is set, we need to skip
-- both the upper and lower case labels
if vim.go.ignorecase then
return c:lower() ~= char:lower()
end
return c ~= char
end, labels)
-- HACK: this will fail if the pattern is an incomplete regex
-- In that case, we skip all labels
if label_count == #labels then
labels = {}
break
end
end
end)
return labels
end
return M

View File

@ -0,0 +1,299 @@
local require = require("flash.require")
local Config = require("flash.config")
local Labeler = require("flash.labeler")
local Repeat = require("flash.repeat")
local Util = require("flash.util")
local M = {}
---@alias Flash.Char.Motion "'f'" | "'F'" | "'t'" | "'T'"
M.motion = "f" ---@type Flash.Char.Motion
M.char = nil ---@type string?
M.jumping = false
M.state = nil ---@type Flash.State?
M.jump_labels = false
---@type table<Flash.Char.Motion, Flash.State.Config>
M.motions = {
f = { label = { after = { 0, 0 }, before = false } },
t = {},
F = {
jump = { inclusive = false },
search = { forward = false },
label = { after = { 0, 0 }, before = false },
},
T = {
jump = { inclusive = false },
search = { forward = false },
label = { before = true, after = false },
},
}
function M.new()
local State = require("flash.state")
local opts = Config.get({
mode = "char",
labeler = M.labeler,
search = {
multi_window = false,
mode = M.mode(M.motion),
max_length = 1,
},
prompt = {
enabled = false,
},
}, M.motions[M.motion])
-- never show the current match label
opts.highlight.groups.current = M.motion:lower() == "f" and opts.highlight.groups.label
or opts.highlight.groups.match
-- exclude the motion labels so we can use them for next/prev
opts.labels = opts.labels:gsub(M.motion:lower(), "")
opts.labels = opts.labels:gsub(M.motion:upper(), "")
return State.new(opts)
end
function M.labeler(matches, state)
if M.jump_labels then
if not state._labeler then
state._labeler = Labeler.new(state)
end
state._labeler:update()
else
-- set to empty label, so that the character will just be highlighted
for _, m in ipairs(matches) do
m.label = ""
end
end
end
---@param motion Flash.Char.Motion
function M.mode(motion)
---@param c string
return function(c)
c = c:gsub("\\", "\\\\")
local pattern ---@type string
if motion == "t" then
pattern = "\\m.\\ze\\V" .. c
elseif motion == "T" then
pattern = "\\V" .. c .. "\\zs\\m."
else
pattern = "\\V" .. c
end
if not Config.get("char").multi_line then
local pos = vim.api.nvim_win_get_cursor(0)
pattern = ("\\%%%dl"):format(pos[1]) .. pattern
end
return pattern
end
end
function M.visible()
return M.state and M.state.visible
end
function M.setup()
Repeat.setup()
local keys = {}
for k, v in pairs(Config.modes.char.keys) do
if vim.g.mapleader ~= v and vim.g.maplocalleader ~= v then
keys[type(k) == "number" and v or k] = v
end
end
-- don't override ;, mappings if they exist
for _, key in ipairs({ ";", "," }) do
local mapping = vim.fn.maparg(key, "n", false, false)
if keys[key] == key and mapping ~= "" then
keys[key] = nil
end
end
for _, key in ipairs({ "f", "F", "t", "T", ";", "," }) do
if keys[key] then
vim.keymap.set({ "n", "x", "o" }, keys[key], function()
M.jumping = true
local autohide = Config.get("char").autohide
if Repeat.is_repeat then
M.jump_labels = false -- never show jump labels when repeating
M.state:jump({ count = vim.v.count1 })
M.state:show()
else
M.jump(key)
end
vim.schedule(function()
M.jumping = false
if M.state and autohide then
M.state:hide()
end
end)
end, {
silent = true,
})
end
end
vim.api.nvim_create_autocmd({ "BufLeave", "CursorMoved", "InsertEnter" }, {
group = vim.api.nvim_create_augroup("flash_char", { clear = true }),
callback = function(event)
local hide = event.event == "InsertEnter" or not M.jumping
if hide and M.state then
M.state:hide()
end
end,
})
vim.on_key(function(key)
if M.state and key == Util.ESC and (vim.fn.mode() == "n" or vim.fn.mode() == "v") then
M.state:hide()
end
end)
end
function M.parse(key)
---@class Flash.Char.Parse
local ret = {
jump = M.next,
actions = {}, ---@type table<string, fun()>
getchar = false,
}
-- repeat last search when hitting the same key
-- don't repeat when executing a macro
if M.visible() and vim.fn.reg_executing() == "" and M.motion:lower() == key:lower() then
ret.actions = M.actions(M.motion)
if ret.actions[key] then
ret.jump = ret.actions[key]
return ret
else
-- no action defined, so clear the state
M.motion = ""
end
end
-- different motion, clear the state
if M.motions[key] and M.motion ~= key then
if M.state then
M.state:hide()
end
M.motion = key
end
ret.actions = M.actions(M.motion)
if M.motions[key] then
ret.getchar = true
else -- ;,
ret.jump = ret.actions[key] or M.next
end
return ret
end
---@param motion Flash.Char.Motion
---@return table<string, fun()>
function M.actions(motion)
local ret = Config.get("char").char_actions(motion)
for key, value in pairs(ret) do
ret[key] = M[value]
end
return ret
end
function M.jump(key)
local parsed = M.parse(key)
if not M.motion then
return
end
local is_op = vim.fn.mode(true):sub(1, 2) == "no"
-- always re-calculate when not visible
M.state = M.visible() and M.state or M.new()
-- get a new target
if parsed.getchar or not M.char then
local char = M.state:get_char()
if char then
M.char = char
else
return M.state:hide()
end
end
-- HACK: When the motion is t or T, we need to set the current position as a valid target
-- but only when we are not repeating
M.current = M.motion:lower() == "t" and parsed.getchar
-- update the state when needed
if M.state.pattern:empty() then
M.state:update({ pattern = M.char })
end
local jump = parsed.jump
M.jump_labels = Config.get("char").jump_labels
jump()
M.state:update({ force = true })
if M.jump_labels then
parsed.actions[Util.CR] = function()
return false
end
M.state:loop({
restore = is_op,
abort = function()
Util.exit()
end,
jump_on_max_length = false,
actions = parsed.actions,
})
end
return M.state
end
M.current = false
function M.right()
return M.state.opts.search.forward and M.next() or M.prev()
end
function M.left()
return M.state.opts.search.forward and M.prev() or M.next()
end
function M.next()
M.state:jump({
count = vim.v.count1,
forward = M.state.opts.search.forward,
current = M.current,
})
M.current = false
return true
end
function M.prev()
M.state:jump({
count = vim.v.count1,
forward = not M.state.opts.search.forward,
current = M.current,
})
M.current = false
-- check if we should enable wrapping.
if not M.state.opts.search.wrap then
local before = M.state:find({ count = 1, forward = false })
if before and (before.pos < M.state.pos) == M.state.opts.search.forward then
M.state.opts.search.wrap = true
M.state._labeler = nil
M.state:update({ force = true })
end
end
return true
end
return M

View File

@ -0,0 +1,163 @@
local require = require("flash.require")
local Config = require("flash.config")
local Jump = require("flash.jump")
local State = require("flash.state")
local Util = require("flash.util")
local M = {}
---@type Flash.State?
M.state = nil
M.op = false
M.enabled = true
---@param enabled? boolean
function M.toggle(enabled)
if enabled == nil then
enabled = not M.enabled
end
if M.enabled == enabled then
return M.enabled
end
M.enabled = enabled
if State.is_search() then
if M.enabled then
M.start()
M.update(false)
elseif M.state then
M.state:hide()
M.state = nil
end
-- redraw to show the change
vim.cmd("redraw")
-- trigger incsearch to update the matches
vim.api.nvim_feedkeys(" " .. Util.BS, "n", true)
end
return M.enabled
end
---@param check_jump? boolean
function M.update(check_jump)
if not M.state then
return
end
local pattern = vim.fn.getcmdline()
-- when doing // or ??, get the pattern from the search register
-- See :h search-commands
if pattern:sub(1, 1) == vim.fn.getcmdtype() then
pattern = vim.fn.getreg("/") .. pattern:sub(2)
end
M.state:update({ pattern = pattern, check_jump = check_jump })
end
function M.start()
M.state = State.new({
mode = "search",
action = M.jump,
search = {
forward = vim.fn.getcmdtype() == "/",
mode = "search",
incremental = vim.go.incsearch,
},
})
if M.op then
M.state.opts.search.multi_window = false
end
end
function M.setup()
local group = vim.api.nvim_create_augroup("flash", { clear = true })
M.enabled = Config.modes.search.enabled or false
local function wrap(fn)
return function(...)
if M.state then
return fn(...)
end
end
end
vim.api.nvim_create_autocmd("CmdlineChanged", {
group = group,
callback = wrap(function()
M.update()
end),
})
vim.api.nvim_create_autocmd("CmdlineLeave", {
group = group,
callback = wrap(function()
M.state:hide()
M.state = nil
end),
})
vim.api.nvim_create_autocmd("CmdlineEnter", {
group = group,
callback = function()
if State.is_search() and M.enabled then
M.start()
M.set_op(vim.fn.mode() == "v")
end
end,
})
vim.api.nvim_create_autocmd("ModeChanged", {
pattern = "*:c",
group = group,
callback = function()
M.set_op(vim.v.event.old_mode:sub(1, 2) == "no" or vim.fn.mode() == "v")
end,
})
end
function M.set_op(op)
M.op = op
if M.op and M.state then
M.state.opts.search.multi_window = false
end
end
---@param self Flash.State
---@param match Flash.Match
function M.jump(match, self)
local pos = match.pos
local search_reg = vim.fn.getreg("/")
-- For operator pending mode, set the search pattern to the
-- first character on the match position
if M.op then
local pos_pattern = ("\\%%%dl\\%%%dc."):format(pos[1], pos[2] + 1)
vim.fn.setcmdline(pos_pattern)
end
-- schedule a <cr> input to trigger the search
vim.schedule(function()
vim.api.nvim_input(M.op and "<cr>" or "<esc>")
end)
-- restore the real search pattern after the search
-- and perform the jump when not in operator pending mode
vim.api.nvim_create_autocmd("CmdlineLeave", {
once = true,
callback = vim.schedule_wrap(function()
-- delete the search pattern.
-- The correct one will be added in `on_jump`
vim.fn.histdel("search", -1)
if M.op then
-- restore original search pattern
vim.fn.setreg("/", search_reg)
else
Jump.jump(match, self)
end
Jump.on_jump(self)
end),
})
end
return M

View File

@ -0,0 +1,181 @@
local Config = require("flash.config")
local Pos = require("flash.search.pos")
local Repeat = require("flash.repeat")
local Util = require("flash.util")
local M = {}
---@class Flash.Match.TS: Flash.Match
---@field node TSNode
---@field depth? number
---@param win window
---@param pos? Pos
function M.get_nodes(win, pos)
local buf = vim.api.nvim_win_get_buf(win)
local line_count = vim.api.nvim_buf_line_count(buf)
pos = pos or Pos()
local nodes = {} ---@type TSNode[]
local ok, tree = pcall(vim.treesitter.get_parser, buf)
if not ok then
vim.notify(
"No treesitter parser for this buffer with filetype=" .. vim.bo[buf].filetype,
vim.log.levels.WARN,
{ title = "flash.nvim" }
)
vim.api.nvim_input("<esc>")
end
if not (ok and tree) then
return {}
end
do
-- get all ranges of the current node and its parents
local node = tree:named_node_for_range({ pos[1] - 1, pos[2], pos[1] - 1, pos[2] }, {
ignore_injections = false,
})
while node do
nodes[#nodes + 1] = node
node = node:parent() ---@type TSNode
end
end
-- convert ranges to matches
---@type Flash.Match.TS[]
local ret = {}
local first = true
---@type table<string,boolean>
local done = {}
for _, node in ipairs(nodes) do
local range = { node:range() }
---@type Flash.Match.TS
local match = {
node = node,
pos = { range[1] + 1, range[2] },
end_pos = { range[3] + 1, range[4] - 1 },
first = first,
}
first = false
-- If the match is at the end of the buffer,
-- then move it to the last character of the last line.
if match.end_pos[1] > line_count then
match.end_pos[1] = line_count
match.end_pos[2] =
#vim.api.nvim_buf_get_lines(buf, match.end_pos[1] - 1, match.end_pos[1], false)[1]
elseif match.end_pos[2] == -1 then
-- If the end points to the start of the next line, move it to the
-- end of the previous line.
-- Otherwise operations include the first character of the next line
local line =
vim.api.nvim_buf_get_lines(buf, match.end_pos[1] - 2, match.end_pos[1] - 1, false)[1]
match.end_pos[1] = match.end_pos[1] - 1
match.end_pos[2] = #line
end
local id = table.concat(vim.tbl_flatten({ match.pos, match.end_pos }), ".")
if not done[id] then
done[id] = true
ret[#ret + 1] = match
end
end
for m, match in ipairs(ret) do
match.pos = Pos(match.pos)
match.end_pos = Pos(match.end_pos)
match.win = win
match.depth = #ret - m
end
return ret
end
---@param win window
---@param state Flash.State
function M.matcher(win, state)
local labels = state:labels()
local ret = M.get_nodes(win, state.pos)
for i = 1, #ret do
ret[i].label = table.remove(labels, 1)
end
return ret
end
---@param opts? Flash.Config
function M.jump(opts)
local state = Repeat.get_state(
"treesitter",
Config.get({ mode = "treesitter" }, opts, {
matcher = M.matcher,
labeler = function() end,
search = { multi_window = false, wrap = true, incremental = false, max_length = 0 },
})
)
---@type Flash.Match?
local current
for _, m in ipairs(state.results) do
---@cast m Flash.Match.TS
if not current or m.depth > current.depth then
current = m
end
end
current = state:jump(current)
state:loop({
abort = function()
vim.cmd([[normal! v]])
end,
actions = {
[";"] = function()
current = state:jump({ match = current, forward = false })
end,
[","] = function()
current = state:jump({ forward = true, match = current })
end,
[Util.CR] = function()
state:jump(current and current.label or nil)
return false
end,
},
jump_on_max_length = false,
})
return state
end
---@param opts? Flash.Config
function M.search(opts)
opts = Config.get({ mode = "treesitter_search" }, opts, {
matcher = function(win, _state, _opts)
local Search = require("flash.search")
local search = Search.new(win, _state)
local matches = {} ---@type Flash.Match[]
for _, m in ipairs(search:get(_opts)) do
-- don't add labels to the search results
m.label = false
table.insert(matches, m)
for _, n in ipairs(M.get_nodes(win, m.pos)) do
-- don't highlight treesitter nodes. Use labels only
n.highlight = false
table.insert(matches, n)
end
end
return matches
end,
jump = { pos = "range" },
})
opts.search.exclude = vim.deepcopy(opts.search.exclude)
table.insert(opts.search.exclude, function(win)
local buf = vim.api.nvim_win_get_buf(win)
return not pcall(vim.treesitter.get_parser, buf)
end)
local state = Repeat.get_state("treesitter-search", opts)
state:loop()
return state
end
return M

View File

@ -0,0 +1,85 @@
local Config = require("flash.config")
---@class Flash.Prompt
---@field win window
---@field buf buffer
local M = {}
local ns = vim.api.nvim_create_namespace("flash_prompt")
function M.visible()
return M.win and vim.api.nvim_win_is_valid(M.win) and M.buf and vim.api.nvim_buf_is_valid(M.buf)
end
function M.show()
if M.visible() then
return
end
require("flash.highlight")
M.buf = vim.api.nvim_create_buf(false, true)
vim.bo[M.buf].buftype = "nofile"
vim.bo[M.buf].bufhidden = "wipe"
vim.bo[M.buf].filetype = "flash_prompt"
local config = vim.deepcopy(Config.prompt.win_config)
if config.width <= 1 then
config.width = config.width * vim.go.columns
end
if config.row < 0 then
config.row = vim.go.lines + config.row
end
if config.col < 0 then
config.col = vim.go.columns + config.col
end
config = vim.tbl_extend("force", config, {
style = "minimal",
focusable = false,
noautocmd = true,
})
M.win = vim.api.nvim_open_win(M.buf, false, config)
vim.wo[M.win].winhighlight = "Normal:FlashPrompt"
end
function M.hide()
if M.win and vim.api.nvim_win_is_valid(M.win) then
vim.api.nvim_win_close(M.win, true)
M.win = nil
end
if M.buf and vim.api.nvim_buf_is_valid(M.buf) then
vim.api.nvim_buf_delete(M.buf, { force = true })
M.buf = nil
end
end
---@param pattern string
function M.set(pattern)
M.show()
local text = vim.deepcopy(Config.prompt.prefix)
text[#text + 1] = { pattern }
local str = ""
for _, item in ipairs(text) do
str = str .. item[1]
end
vim.api.nvim_buf_set_lines(M.buf, 0, -1, false, { str })
vim.api.nvim_buf_clear_namespace(M.buf, ns, 0, -1)
local col = 0
for _, item in ipairs(text) do
local width = vim.fn.strlen(item[1])
if item[2] then
vim.api.nvim_buf_set_extmark(M.buf, ns, 0, col, {
hl_group = item[2],
end_col = col + width,
})
end
col = col + width
end
end
return M

View File

@ -0,0 +1,401 @@
---@class Flash.Rainbow
---@field cache table<string, string>
---@field count number
---@field shade number
local M = {}
---@type table<string,true>
M.hl = {}
function M.setup()
if M.did_setup then
return
end
M.did_setup = true
vim.api.nvim_create_autocmd("ColorScheme", {
callback = function()
M.hl = {}
end,
})
end
---@param state Flash.State
function M.new(state)
local self = setmetatable({}, { __index = M })
self.cache = {}
self.count = 0
self.shade = state.opts.label.rainbow.shade
return self
end
---@param match Flash.Match
function M:get(match)
local buf = vim.api.nvim_win_get_buf(match.win)
local id = match.pos:id(buf)
if match.depth then
id = id .. ":" .. tostring(match.depth)
end
if not self.cache[id] then
self.count = self.count + 1
self.cache[id] = M.get_color(self.count, self.shade)
end
return self.cache[id]
end
---@param idx number
---@param shade number
function M.get_color(idx, shade)
M.setup()
idx = (idx - 1) % #M.rainbow + 1
local color = M.rainbow[idx]
shade = (shade or 5) * 100
local bg = vim.tbl_get(M.colors, color, shade)
if bg then
local hl = "FlashColor" .. color .. shade
if not M.hl[hl] then
M.hl[hl] = true
local bg_shade = shade == 500 and 950 or shade < 500 and 900 or 50
local fg = vim.tbl_get(M.colors, color, bg_shade)
vim.api.nvim_set_hl(0, hl, { bg = "#" .. bg, fg = "#" .. fg, bold = true })
end
return hl
end
end
M.rainbow = {
-- "slate",
-- "gray",
-- "zinc",
-- "neutral",
-- "stone",
"red",
-- "orange",
"amber",
-- "yellow",
"lime",
"green",
-- "emerald",
"teal",
"cyan",
-- "sky",
"blue",
-- "indigo",
"violet",
-- "purple",
"fuchsia",
-- "pink",
"rose",
}
M.colors = {
slate = {
[50] = "f8fafc",
[100] = "f1f5f9",
[200] = "e2e8f0",
[300] = "cbd5e1",
[400] = "94a3b8",
[500] = "64748b",
[600] = "475569",
[700] = "334155",
[800] = "1e293b",
[900] = "0f172a",
[950] = "020617",
},
gray = {
[50] = "f9fafb",
[100] = "f3f4f6",
[200] = "e5e7eb",
[300] = "d1d5db",
[400] = "9ca3af",
[500] = "6b7280",
[600] = "4b5563",
[700] = "374151",
[800] = "1f2937",
[900] = "111827",
[950] = "030712",
},
zinc = {
[50] = "fafafa",
[100] = "f4f4f5",
[200] = "e4e4e7",
[300] = "d4d4d8",
[400] = "a1a1aa",
[500] = "71717a",
[600] = "52525b",
[700] = "3f3f46",
[800] = "27272a",
[900] = "18181b",
[950] = "09090B",
},
neutral = {
[50] = "fafafa",
[100] = "f5f5f5",
[200] = "e5e5e5",
[300] = "d4d4d4",
[400] = "a3a3a3",
[500] = "737373",
[600] = "525252",
[700] = "404040",
[800] = "262626",
[900] = "171717",
[950] = "0a0a0a",
},
stone = {
[50] = "fafaf9",
[100] = "f5f5f4",
[200] = "e7e5e4",
[300] = "d6d3d1",
[400] = "a8a29e",
[500] = "78716c",
[600] = "57534e",
[700] = "44403c",
[800] = "292524",
[900] = "1c1917",
[950] = "0a0a0a",
},
red = {
[50] = "fef2f2",
[100] = "fee2e2",
[200] = "fecaca",
[300] = "fca5a5",
[400] = "f87171",
[500] = "ef4444",
[600] = "dc2626",
[700] = "b91c1c",
[800] = "991b1b",
[900] = "7f1d1d",
[950] = "450a0a",
},
orange = {
[50] = "fff7ed",
[100] = "ffedd5",
[200] = "fed7aa",
[300] = "fdba74",
[400] = "fb923c",
[500] = "f97316",
[600] = "ea580c",
[700] = "c2410c",
[800] = "9a3412",
[900] = "7c2d12",
[950] = "431407",
},
amber = {
[50] = "fffbeb",
[100] = "fef3c7",
[200] = "fde68a",
[300] = "fcd34d",
[400] = "fbbf24",
[500] = "f59e0b",
[600] = "d97706",
[700] = "b45309",
[800] = "92400e",
[900] = "78350f",
[950] = "451a03",
},
yellow = {
[50] = "fefce8",
[100] = "fef9c3",
[200] = "fef08a",
[300] = "fde047",
[400] = "facc15",
[500] = "eab308",
[600] = "ca8a04",
[700] = "a16207",
[800] = "854d0e",
[900] = "713f12",
[950] = "422006",
},
lime = {
[50] = "f7fee7",
[100] = "ecfccb",
[200] = "d9f99d",
[300] = "bef264",
[400] = "a3e635",
[500] = "84cc16",
[600] = "65a30d",
[700] = "4d7c0f",
[800] = "3f6212",
[900] = "365314",
[950] = "1a2e05",
},
green = {
[50] = "f0fdf4",
[100] = "dcfce7",
[200] = "bbf7d0",
[300] = "86efac",
[400] = "4ade80",
[500] = "22c55e",
[600] = "16a34a",
[700] = "15803d",
[800] = "166534",
[900] = "14532d",
[950] = "052e16",
},
emerald = {
[50] = "ecfdf5",
[100] = "d1fae5",
[200] = "a7f3d0",
[300] = "6ee7b7",
[400] = "34d399",
[500] = "10b981",
[600] = "059669",
[700] = "047857",
[800] = "065f46",
[900] = "064e3b",
[950] = "022c22",
},
teal = {
[50] = "f0fdfa",
[100] = "ccfbf1",
[200] = "99f6e4",
[300] = "5eead4",
[400] = "2dd4bf",
[500] = "14b8a6",
[600] = "0d9488",
[700] = "0f766e",
[800] = "115e59",
[900] = "134e4a",
[950] = "042f2e",
},
cyan = {
[50] = "ecfeff",
[100] = "cffafe",
[200] = "a5f3fc",
[300] = "67e8f9",
[400] = "22d3ee",
[500] = "06b6d4",
[600] = "0891b2",
[700] = "0e7490",
[800] = "155e75",
[900] = "164e63",
[950] = "083344",
},
sky = {
[50] = "f0f9ff",
[100] = "e0f2fe",
[200] = "bae6fd",
[300] = "7dd3fc",
[400] = "38bdf8",
[500] = "0ea5e9",
[600] = "0284c7",
[700] = "0369a1",
[800] = "075985",
[900] = "0c4a6e",
[950] = "082f49",
},
blue = {
[50] = "eff6ff",
[100] = "dbeafe",
[200] = "bfdbfe",
[300] = "93c5fd",
[400] = "60a5fa",
[500] = "3b82f6",
[600] = "2563eb",
[700] = "1d4ed8",
[800] = "1e40af",
[900] = "1e3a8a",
[950] = "172554",
},
indigo = {
[50] = "eef2ff",
[100] = "e0e7ff",
[200] = "c7d2fe",
[300] = "a5b4fc",
[400] = "818cf8",
[500] = "6366f1",
[600] = "4f46e5",
[700] = "4338ca",
[800] = "3730a3",
[900] = "312e81",
[950] = "1e1b4b",
},
violet = {
[50] = "f5f3ff",
[100] = "ede9fe",
[200] = "ddd6fe",
[300] = "c4b5fd",
[400] = "a78bfa",
[500] = "8b5cf6",
[600] = "7c3aed",
[700] = "6d28d9",
[800] = "5b21b6",
[900] = "4c1d95",
[950] = "2e1065",
},
purple = {
[50] = "faf5ff",
[100] = "f3e8ff",
[200] = "e9d5ff",
[300] = "d8b4fe",
[400] = "c084fc",
[500] = "a855f7",
[600] = "9333ea",
[700] = "7e22ce",
[800] = "6b21a8",
[900] = "581c87",
[950] = "3b0764",
},
fuchsia = {
[50] = "fdf4ff",
[100] = "fae8ff",
[200] = "f5d0fe",
[300] = "f0abfc",
[400] = "e879f9",
[500] = "d946ef",
[600] = "c026d3",
[700] = "a21caf",
[800] = "86198f",
[900] = "701a75",
[950] = "4a044e",
},
pink = {
[50] = "fdf2f8",
[100] = "fce7f3",
[200] = "fbcfe8",
[300] = "f9a8d4",
[400] = "f472b6",
[500] = "ec4899",
[600] = "db2777",
[700] = "be185d",
[800] = "9d174d",
[900] = "831843",
[950] = "500724",
},
rose = {
[50] = "fff1f2",
[100] = "ffe4e6",
[200] = "fecdd3",
[300] = "fda4af",
[400] = "fb7185",
[500] = "f43f5e",
[600] = "e11d48",
[700] = "be123c",
[800] = "9f1239",
[900] = "881337",
[950] = "4c0519",
},
}
return M

View File

@ -0,0 +1,55 @@
local require = require("flash.require")
local State = require("flash.state")
local M = {}
---@type {is_repeat:boolean, fn:fun()}[]
M._funcs = {}
M._repeat = nil
-- Sets the current operatorfunc to the given function.
function M.set(fn)
vim.go.operatorfunc = [[{x -> x}]]
local visual = vim.fn.mode() == "v"
vim.cmd("normal! g@l")
if visual then
vim.cmd("normal! gv")
end
M._repeat = fn
vim.go.operatorfunc = [[v:lua.require'flash.repeat'._repeat]]
end
M.is_repeat = false
function M.setup()
if M._did_setup then
return
end
M._did_setup = true
vim.on_key(function(key)
if key == "." and vim.fn.reg_executing() == "" and vim.fn.reg_recording() == "" then
M.is_repeat = true
vim.schedule(function()
M.is_repeat = false
end)
end
end)
end
---@type table<string, Flash.State>
M._states = {}
---@param mode string
---@param opts? Flash.State.Config
function M.get_state(mode, opts)
M.setup()
local last = M._states[mode]
if (M.is_repeat or (opts and opts.continue)) and last then
last:show()
return last
end
M._states[mode] = State.new(opts)
return M._states[mode]
end
return M

View File

@ -0,0 +1,25 @@
return function(module)
local mod = nil
local function load()
if not mod then
mod = require(module)
package.loaded[module] = mod
end
return mod
end
-- if already loaded, return the module
-- otherwise return a lazy module
return type(package.loaded[module]) == "table" and package.loaded[module]
or setmetatable({}, {
__index = function(_, key)
return load()[key]
end,
__newindex = function(_, key, value)
load()[key] = value
end,
__call = function(_, ...)
return load()(...)
end,
})
end

View File

@ -0,0 +1,117 @@
local require = require("flash.require")
local Hacks = require("flash.hacks")
local Matcher = require("flash.search.matcher")
local Pos = require("flash.search.pos")
---@class Flash.Search: Flash.Matcher
---@field state Flash.State
---@field win window
local M = {}
M.__index = M
---@param win number
---@param state Flash.State
function M.new(win, state)
local self = setmetatable({}, M)
self.state = state
self.win = win
return self
end
---@param flags? string
---@return Flash.Match?
function M:_next(flags)
flags = flags or ""
local ok, pos = pcall(vim.fn.searchpos, self.state.pattern.search, flags or "")
-- incomplete or invalid pattern
if not ok then
return
end
if pos[1] == 0 then
return
end
pos = Pos({ pos[1], pos[2] - 1 })
return { win = self.win, pos = pos, end_pos = Hacks.get_end_pos(pos) }
end
---@param pos Pos
---@param fn function
function M:_call(pos, fn)
pos = Pos(pos)
local view = vim.api.nvim_win_call(self.win, vim.fn.winsaveview)
local buf = vim.api.nvim_win_get_buf(self.win)
local line_count = vim.api.nvim_buf_line_count(buf)
if pos[1] > line_count then
pos[1] = line_count
local line = vim.api.nvim_buf_get_lines(buf, pos[1] - 1, pos[1], false)[1]
pos[2] = #line - 1
end
vim.api.nvim_win_set_cursor(self.win, pos)
---@type boolean, any?
local ok, err
vim.api.nvim_win_call(self.win, function()
ok, err = pcall(fn)
vim.fn.winrestview(view)
end)
return not ok and error(err) or err
end
---@param opts? {from?:Pos, to?:Pos}
function M:get(opts)
if self.state.pattern:empty() then
return {}
end
opts = opts or {}
opts.from = opts.from and Pos(opts.from) or nil
opts.to = opts.to and Pos(opts.to) or nil
---@type Flash.Match[]
local ret = {}
self:_call(opts.from or { 1, 0 }, function()
local next = self:_next("cW")
while next and (not opts.to or next.pos <= opts.to) do
table.insert(ret, next)
next = self:_next("W")
end
end)
return ret
end
-- Moves the results cursor by `amount` (default 1) and wraps around.
-- When forward is `nil` it uses the current search direction.
-- Otherwise it uses the given direction.
---@param opts? Flash.Match.Find
function M:find(opts)
if self.state.pattern:empty() then
return
end
opts = Matcher.defaults(opts)
local flags = (opts.forward and "" or "b")
.. (opts.wrap and "w" or "W")
.. ((opts.count == 0 or opts.current) and "c" or "")
if opts.match then
opts.pos = opts.match.pos
end
---@type Flash.Match?
local ret
self:_call(opts.pos, function()
for _ = 1, math.max(opts.count, 1) do
ret = self:_next(flags)
flags = flags:gsub("c", "")
end
end)
if not ret or (opts.count == 0 and ret.pos ~= opts.pos) then
return
end
return ret
end
return M

View File

@ -0,0 +1,179 @@
local Pos = require("flash.search.pos")
---@class Flash.Match
---@field win window
---@field pos Pos -- (1,0) indexed
---@field end_pos Pos -- (1,0) indexed
---@field label? string|false -- set to false to disable label
---@field highlight? boolean
---@field fold? number
---@alias Flash.Match.Find {forward?:boolean, wrap?:boolean, count?:number, pos?: Pos, match?:Flash.Match, current?:boolean}
---@class Flash.Matcher
---@field win window
---@field get fun(self, opts?: {from?:Pos, to?:Pos}): Flash.Match[]
---@field find fun(self, opts?: Flash.Match.Find): Flash.Match
---@field labels fun(self, labels: string[]): string[]
---@field update? fun(self)
---@class Flash.Matcher.Custom: Flash.Matcher
---@field matches Flash.Match[]
local M = {}
M.__index = M
function M.new(win)
local self = setmetatable({}, M)
self.matches = {}
self.win = win
return self
end
---@param fn fun(win: window, state:Flash.State, opts: {from:Pos, to:Pos}): Flash.Match[]
function M.from(fn)
---@param win window
---@param state Flash.State
return function(win, state)
local ret = M.new(win)
ret.get = function(self, opts)
local matches = fn(win, state, opts)
if state.opts.filter then
matches = state.opts.filter(matches, state) or matches
end
self:set(matches)
return M.get(self, opts)
end
return ret
end
end
---@param ...? Flash.Match.Find
---@return Flash.Match.Find
function M.defaults(...)
local other = vim.tbl_filter(function(k)
return k ~= nil
end, { ... })
local opts = vim.tbl_extend("force", {
pos = vim.api.nvim_win_get_cursor(0),
forward = true,
wrap = true,
count = 1,
}, {}, unpack(other))
opts.pos = Pos(opts.pos)
return opts
end
---@param opts? Flash.Match.Find
function M:find(opts)
opts = M.defaults(opts)
if opts.count == 0 then
for _, match in ipairs(self.matches) do
if match.pos == opts.pos then
return match
end
end
return
end
---@type number?
local idx
if opts.match then
for m, match in ipairs(self.matches) do
if match.pos == opts.match.pos and match.end_pos == opts.match.end_pos then
idx = m + (opts.forward and 1 or -1)
break
end
end
elseif opts.forward then
for i = 1, #self.matches, 1 do
if self.matches[i].pos > opts.pos then
idx = i
break
end
end
else
for i = #self.matches, 1, -1 do
if self.matches[i].pos < opts.pos then
idx = i
break
end
end
end
if not idx then
if not opts.wrap then
return
end
idx = opts.forward and 1 or #self.matches
end
if opts.forward then
idx = idx + opts.count - 1
else
idx = idx - opts.count + 1
end
if opts.wrap then
idx = (idx - 1) % #self.matches + 1
end
return self.matches[idx]
end
---@param labels string[]
function M:labels(labels)
return labels
end
---@param opts? {from?:Pos, to?:Pos}
function M:get(opts)
return M.filter(self.matches, opts)
end
---@param matches Flash.Match[]
---@param opts? {from?:Pos, to?:Pos}
function M.filter(matches, opts)
opts = opts or {}
opts.from = opts.from and Pos(opts.from)
opts.to = opts.to and Pos(opts.to)
---@param match Flash.Match
return vim.tbl_filter(function(match)
if opts.from and match.end_pos < opts.from then
return false
end
if opts.to and match.pos > opts.to then
return false
end
return true
end, matches)
end
---@param matches Flash.Match[]
function M:set(matches)
for _, match in ipairs(matches) do
match.pos = Pos(match.pos)
match.end_pos = Pos(match.end_pos)
match.win = match.win or self.win
end
table.sort(matches, function(a, b)
if a.win ~= b.win then
return a.win < b.win
end
if a.pos ~= b.pos then
return a.pos < b.pos
end
local da = a.depth or 0
local db = b.depth or 0
if da ~= db then
return da < db
end
return a.end_pos < b.end_pos
end)
self.matches = matches
end
return M

View File

@ -0,0 +1,108 @@
local Util = require("flash.util")
---@class Flash.Pattern
---@field pattern string
---@field search string
---@field skip string
---@field trigger string
---@field mode Flash.Pattern.Mode
---@operator call:string Returns the input pattern
local M = {}
M.__index = M
---@alias Flash.Pattern.Mode "exact" | "fuzzy" | "search" | (fun(input:string):string,string?)
---@param pattern string
---@param mode Flash.Pattern.Mode
---@param trigger string
function M.new(pattern, mode, trigger)
local self = setmetatable({}, M)
self.mode = mode
self.trigger = trigger or ""
self:set(pattern or "")
return self
end
function M:__eq(other)
return other and other.pattern == self.pattern and other.mode == self.mode
end
function M:clone()
return M.new(self.pattern, self.mode, self.trigger)
end
function M:empty()
return self.pattern == ""
end
---@param pattern string
---@return boolean updated
function M:set(pattern)
if pattern ~= self.pattern then
self.pattern = pattern
if pattern == "" then
self.search = ""
self.skip = ""
else
if self.trigger ~= "" and pattern:sub(-1) == self.trigger then
pattern = pattern:sub(1, -2)
end
self.search, self.skip = M._get(pattern, self.mode)
end
return false
end
return true
end
---@param char string
function M:extend(char)
if char == Util.BS then
return self.pattern:sub(1, -2)
end
return self.pattern .. char
end
---@return string the input pattern
function M:__call()
return self.pattern
end
---@param pattern string
---@param mode Flash.Pattern.Mode
---@private
function M._get(pattern, mode)
local skip ---@type string?
if type(mode) == "function" then
pattern, skip = mode(pattern)
elseif mode == "exact" then
pattern, skip = M._exact(pattern)
elseif mode == "fuzzy" then
pattern, skip = M._fuzzy(pattern)
end
return pattern, skip or pattern
end
---@param pattern string
function M._exact(pattern)
return "\\V" .. pattern:gsub("\\", "\\\\")
end
---@param opts? {ignorecase: boolean, whitespace:boolean}
function M._fuzzy(pattern, opts)
opts = vim.tbl_deep_extend("force", {
ignorecase = vim.go.ignorecase,
whitespace = false,
}, opts or {})
local sep = opts.whitespace and ".\\{-}" or "\\[^\\ ]\\{-}"
---@param c string
local chars = vim.tbl_map(function(c)
return c == "\\" and "\\\\" or c
end, vim.fn.split(pattern, "\\zs"))
local ret = "\\V" .. table.concat(chars, sep) .. (opts.ignorecase and "\\c" or "\\C")
return ret, ret .. sep
end
return M

View File

@ -0,0 +1,85 @@
---@class Pos
---@field row number
---@field col number
---@field [1] number
---@field [2] number
---@overload fun(pos?: number[] | { row: number, col: number } | number): Pos
local P = {}
---@param pos? number[] | { row: number, col: number } | number
function P.new(pos)
if pos == nil then
pos = vim.api.nvim_win_get_cursor(0)
elseif type(pos) == "number" then
pos = vim.api.nvim_win_get_cursor(pos)
end
if getmetatable(pos) == P then
return pos
end
local self = setmetatable({}, P)
self[1] = pos[1] or pos.row
self[2] = pos[2] or pos.col
return self
end
function P:__index(key)
if key == "row" then
return rawget(self, 1)
elseif key == "col" then
return rawget(self, 2)
end
return P[key]
end
function P:__newindex(key, value)
if key == "row" then
rawset(self, 1, value)
elseif key == "col" then
rawset(self, 2, value)
else
rawset(self, key, value)
end
end
function P:__eq(other)
return self[1] == other[1] and self[2] == other[2]
end
function P:__tostring()
return ("[%d, %d]"):format(self[1], self[2])
end
function P:id(buf)
return table.concat({ buf, self[1], self[2] }, ":")
end
function P:dist(other)
return math.abs(self[1] - other[1]) + math.abs(self[2] - other[2])
end
function P:__add(other)
other = P(other)
return P.new({ self[1] + other[1], self[2] + other[2] })
end
function P:__sub(other)
other = P(other)
return P.new({ self[1] - other[1], self[2] - other[2] })
end
function P:__lt(other)
other = P(other)
return self[1] < other[1] or (self[1] == other[1] and self[2] < other[2])
end
function P:__le(other)
other = P(other)
return self < other or self == other
end
return setmetatable(P, {
__call = function(_, pos)
return P.new(pos)
end,
})

View File

@ -0,0 +1,420 @@
local require = require("flash.require")
local Cache = require("flash.cache")
local Config = require("flash.config")
local Hacks = require("flash.hacks")
local Highlight = require("flash.highlight")
local Jump = require("flash.jump")
local Matcher = require("flash.search.matcher")
local Pattern = require("flash.search.pattern")
local Prompt = require("flash.prompt")
local Rainbow = require("flash.rainbow")
local Search = require("flash.search")
local Util = require("flash.util")
---@class Flash.State.Config: Flash.Config
---@field matcher? fun(win: window, state:Flash.State, pos: {from:Pos, to:Pos}): Flash.Match[]
---@field filter? fun(matches:Flash.Match[], state:Flash.State): Flash.Match[]
---@field pattern? string
---@field labeler? fun(matches:Flash.Match[], state:Flash.State)
---@field actions? table<string, fun(state:Flash.State, char:string):boolean?>
---@class Flash.State
---@field win window
---@field wins window[]
---@field cache Flash.Cache
---@field pos Pos
---@field view any
---@field results Flash.Match[]
---@field target? Flash.Match
---@field pattern Flash.Pattern
---@field opts Flash.State.Config
---@field labeler fun(matches:Flash.Match[], state:Flash.State)
---@field visible boolean
---@field matcher fun(win: window, state:Flash.State): Flash.Matcher
---@field matchers Flash.Matcher[]
---@field restore_windows? fun()
---@field rainbow? Flash.Rainbow
---@field ns number
---@field langmap table<string, string>
local M = {}
M.__index = M
---@type table<Flash.State, boolean>
M._states = setmetatable({}, { __mode = "k" })
function M.setup()
if M._did_setup then
return
end
M._did_setup = true
local ns = vim.api.nvim_create_namespace("flash")
vim.api.nvim_set_decoration_provider(ns, {
on_start = function()
for state in pairs(M._states) do
if state.visible then
local ok, err = pcall(state.update, state)
if not ok then
vim.schedule(function()
vim.notify(
"Flash error during redraw:\n" .. err,
vim.log.levels.ERROR,
{ title = "flash.nvim" }
)
end)
end
end
end
end,
})
end
---@param char string
function M:lmap(char)
return vim.bo.iminsert == 1 and self.langmap[char] or char
end
function M:get_char()
local ret = Util.get_char()
return ret and self:lmap(ret) or nil
end
function M:labels()
local labels = self.opts.labels
if self.opts.label.uppercase then
labels = labels .. self.opts.labels:upper()
end
local list = vim.fn.split(labels, "\\zs")
local ret = {} ---@type string[]
local added = {} ---@type table<string, boolean>
for _, l in ipairs(vim.fn.split(self.opts.label.exclude, "\\zs")) do
added[l] = true
end
for _, l in ipairs(list) do
if not added[l] then
added[l] = true
ret[#ret + 1] = self:lmap(l)
end
end
return ret
end
function M.is_search()
local t = vim.fn.getcmdtype()
return t == "/" or t == "?"
end
---@param opts? Flash.State.Config
function M.new(opts)
M.setup()
local self = setmetatable({}, M)
self.opts = Config.get(opts)
self.langmap = {}
if vim.bo.iminsert == 1 then
local lmap = vim.api.nvim_buf_get_keymap(0, "l")
for _, m in ipairs(lmap) do
if m.lhs ~= "" then
self.langmap[m.lhs] = m.rhs
end
end
end
self.results = {}
self.matchers = {}
self.wins = {}
self.matcher = self.opts.matcher
if type(self.matcher) == "function" then
self.matcher = Matcher.from(self.opts.matcher)
elseif self.matcher == nil then
self.matcher = Search.new
end
self.pattern = Pattern.new(self.opts.pattern, self.opts.search.mode, self.opts.search.trigger)
self.visible = true
self.cache = Cache.new(self)
self.labeler = self.opts.labeler or require("flash.labeler").new(self):labeler()
self.ns = vim.api.nvim_create_namespace(self.opts.ns or "flash")
M._states[self] = true
if self.opts.label.rainbow.enabled then
self.rainbow = Rainbow.new(self)
end
self:update()
return self
end
---@param target? string|Flash.Match.Find|Flash.Match
---@return Flash.Match?
function M:jump(target)
local match ---@type Flash.Match?
if type(target) == "string" then
match = self:find({ label = target })
elseif target and target.end_pos then
match = target
elseif target then
match = self:find(target)
else
match = self.target
end
if match then
if self.opts.action then
self.opts.action(match, self)
else
Jump.jump(match, self)
Jump.on_jump(self)
end
return match
end
end
-- Will restore all window views
function M:restore()
if self.restore_windows then
self.restore_windows()
end
end
function M:get_matcher(win)
self.matchers[win] = self.matchers[win] or self.matcher(win, self)
return self.matchers[win]
end
---@param opts? Flash.Match.Find | {label?:string, pos?: Pos}
function M:find(opts)
if opts and opts.label then
for _, m in ipairs(self.results) do
if m.label == opts.label then
return m
end
end
return
end
opts = Matcher.defaults({
forward = self.opts.search.forward,
wrap = self.opts.search.wrap,
}, opts)
local matcher = self:get_matcher(self.win)
local ret = matcher:find(opts)
if ret then
for _, m in ipairs(self.results) do
if m.pos == ret.pos and m.end_pos == ret.end_pos then
return m
end
end
end
return ret
end
-- Checks if the given pattern is a jump label and jumps to it.
---@param pattern string
function M:check_jump(pattern)
if not self.visible then
return
end
if self.opts.search.trigger ~= "" and self.pattern():sub(-1) ~= self.opts.search.trigger then
return
end
local chars = vim.fn.strchars(pattern)
if
pattern:find(self.pattern(), 1, true) == 1 and chars == vim.fn.strchars(self.pattern()) + 1
then
local label = vim.fn.strcharpart(pattern, chars - 1, 1)
if self:jump(label) then
return true
end
end
end
---@param opts? {pattern:string, force:boolean, check_jump:boolean}
---@return boolean? abort `true` if the search was aborted
function M:update(opts)
opts = opts or {}
if opts.pattern then
-- abort if pattern is a jump label
if opts.check_jump ~= false and self:check_jump(opts.pattern) then
return true
end
self.pattern:set(opts.pattern)
end
if not self.visible then
return
end
if self.cache:update() or opts.force then
self:_update()
end
end
function M:hide()
if self.visible then
self.visible = false
Highlight.clear(self.ns)
end
end
function M:show()
if not self.visible then
self.visible = true
-- force cache to update win and position
self.win = nil
self:update({ force = true })
end
end
function M:_update()
-- This is needed because we trigger searches during redraw.
-- We need to save the state of the incsearch so that current match
-- will still be displayed correctly.
if M.is_search() then
Hacks.save_incsearch_state()
end
self.results = {}
local done = {} ---@type table<string, boolean>
---@type Flash.Matcher[]
local matchers = {}
for _, win in ipairs(self.wins) do
local buf = vim.api.nvim_win_get_buf(win)
matchers[win] = self:get_matcher(win)
local state = self.cache:get_state(win)
for _, m in ipairs(state and state.matches or {}) do
local id = m.pos:id(buf) .. m.end_pos:id(buf)
if not done[id] then
done[id] = true
table.insert(self.results, m)
end
end
end
self.matchers = matchers
for _, match in ipairs(self.results) do
vim.api.nvim_win_call(match.win, function()
local fold = vim.fn.foldclosed(match.pos[1])
match.fold = fold ~= -1 and fold or nil
end)
end
self:update_target()
self.labeler(self.results, self)
if M.is_search() then
Hacks.restore_incsearch_state()
end
Highlight.update(self)
end
function M:update_target()
-- set target to next match.
-- When not using incremental search,
-- we need to set the target to the previous match
self.target = self:find({
pos = self.pos,
count = vim.v.count1,
})
local info = vim.fn.getwininfo(self.win)[1]
local function is_visible()
return self.target and self.target.pos[1] >= info.topline and self.target.pos[1] <= info.botline
end
if self.opts.search.incremental then
-- only update cursor if the target is not visible
-- and we are not activated
if self.target and not self.is_search() then
vim.api.nvim_win_set_cursor(self.win, self.target.pos)
end
elseif not is_visible() then
self.target = self:find({
pos = self.pos,
count = vim.v.count1,
forward = not self.opts.search.forward,
})
if not is_visible() then
self.target = nil
end
end
end
---@class Flash.Step.Options
---@field actions? table<string, fun(state:Flash.State, char:string):boolean?>
---@field restore? boolean
---@field abort? fun()
---@field jump_on_max_length? boolean
---@param opts? Flash.Step.Options
function M:step(opts)
opts = opts or {}
if self.opts.prompt.enabled and not M.is_search() then
Prompt.set(self.pattern())
end
local actions = opts.actions or self.opts.actions or {}
local c = self:get_char()
if c == nil then
vim.api.nvim_input("<esc>")
if opts.restore ~= false then
self:restore()
end
if opts.abort then
opts.abort()
end
return
elseif actions[c] then
local ret = actions[c](self, c)
if ret == nil then
return true
end
return ret
-- jump to first
elseif c == Util.CR then
self:jump()
return
end
local orig = self.pattern()
-- break if we jumped
if self:update({ pattern = self.pattern:extend(c) }) then
return
end
-- when we exceed max length, either jump to the label,
-- or input the last key and break
if self.opts.search.max_length and #self.pattern() > self.opts.search.max_length then
self:update({ pattern = orig })
if opts.jump_on_max_length ~= false then
self:jump()
end
vim.api.nvim_input(c)
return
end
-- exit if no results and not in regular search mode
if #self.results == 0 and not self.pattern:empty() and self.pattern.mode ~= 'search' then
if self.opts.search.incremental then
vim.api.nvim_input(c)
end
return
end
-- autojump if only one result
if #self.results == 1 and self.opts.jump.autojump then
self:jump()
return
end
return true
end
---@param opts? Flash.Step.Options
function M:loop(opts)
while self:step(opts) do
end
self:hide()
Prompt.hide()
end
return M

View File

@ -0,0 +1,105 @@
local Hacks = require("flash.hacks")
local require = require("flash.require")
local M = {}
function M.t(str)
return vim.api.nvim_replace_termcodes(str, true, true, true)
end
M.CR = M.t("<cr>")
M.ESC = M.t("<esc>")
M.BS = M.t("<bs>")
M.EXIT = M.t("<C-\\><C-n>")
M.LUA_CALLBACK = "\x80\253g"
M.CMD = "\x80\253h"
function M.exit()
vim.api.nvim_feedkeys(M.EXIT, "nx", false)
vim.api.nvim_feedkeys(M.ESC, "n", false)
end
---@param buf number
---@param pos number[] (1,0)-indexed position
---@param offset number[]
---@return number[] (1,0)-indexed position
function M.offset_pos(buf, pos, offset)
local row = pos[1] + offset[1]
local ok, lines = pcall(vim.api.nvim_buf_get_lines, buf, row - 1, row, true)
if not ok or lines == nil then
-- fallback to old behavior if anything wrong happens
return { row, math.max(pos[2] + offset[2], 0) }
end
local line = lines[1]
local charidx = vim.fn.charidx(line, pos[2])
local col = vim.fn.byteidx(line, charidx + offset[2])
return { row, math.max(col, 0) }
end
function M.get_char()
Hacks.setcursor()
vim.cmd("redraw")
local ok, ret = pcall(vim.fn.getcharstr)
return ok and ret ~= M.ESC and ret or nil
end
function M.layout_wins()
local queue = { vim.fn.winlayout() }
---@type table<window, window>
local wins = {}
while #queue > 0 do
local node = table.remove(queue)
if node[1] == "leaf" then
wins[node[2]] = node[2]
else
vim.list_extend(queue, node[2])
end
end
return wins
end
function M.save_layout()
local current_win = vim.api.nvim_get_current_win()
local wins = M.layout_wins()
---@type table<window, table>
local state = {}
for _, win in pairs(wins) do
state[win] = vim.api.nvim_win_call(win, vim.fn.winsaveview)
end
return function()
for win, s in pairs(state) do
if vim.api.nvim_win_is_valid(win) then
local buf = vim.api.nvim_win_get_buf(win)
-- never restore terminal buffers to prevent flickering
if vim.bo[buf].buftype ~= "terminal" then
pcall(vim.api.nvim_win_call, win, function()
vim.fn.winrestview(s)
end)
end
end
end
vim.api.nvim_set_current_win(current_win)
state = {}
end
end
---@param done fun():boolean
---@param on_done fun()
function M.on_done(done, on_done)
local check = assert(vim.loop.new_check())
local fn = function()
if check:is_closing() then
return
end
if done() then
check:stop()
check:close()
on_done()
end
end
check:start(vim.schedule_wrap(fn))
end
return M

View File

@ -0,0 +1 @@
std="vim"

View File

@ -0,0 +1,5 @@
indent_type = "Spaces"
indent_width = 2
column_width = 100
[sort_requires]
enabled = true

View File

@ -0,0 +1,90 @@
local Char = require("flash.plugins.char")
local assert = require("luassert")
require("flash").setup()
describe("char", function()
local function set(text, pos)
local lines = vim.split(vim.trim(text), "\n")
lines = vim.tbl_map(function(line)
return vim.trim(line)
end, lines)
vim.api.nvim_buf_set_lines(1, 0, -1, false, lines)
vim.api.nvim_win_set_cursor(0, pos or { 1, 0 })
end
before_each(function()
set("abc_xyz", { 1, 3 })
local state = require("flash.plugins.char").state
if state then
state:hide()
end
end)
local function get()
return table.concat(vim.api.nvim_buf_get_lines(1, 0, -1, false), "\n")
end
--- tests for deletes with ftFT motions
--- test always runs on input "abc_xyz"
--- with cursor at position { 1, 3 }
local tests = {
-- f
{ motion = "dfx", result = "abcyz" },
{ motion = "dfz", result = "abc" },
{ motion = "df_", result = "abc_xyz" },
{ motion = "dfa", result = "abc_xyz" },
-- t
{ motion = "dtx", result = "abcxyz" },
{ motion = "dtz", result = "abcz" },
{ motion = "dt_", result = "abc_xyz" },
{ motion = "dta", result = "abc_xyz" },
-- F
{ motion = "dFa", result = "_xyz" },
{ motion = "dFc", result = "ab_xyz" },
{ motion = "dF_", result = "abc_xyz" },
{ motion = "dFx", result = "abc_xyz" },
-- T
{ motion = "dTa", result = "a_xyz" },
{ motion = "dTc", result = "abc_xyz" },
{ motion = "dT_", result = "abc_xyz" },
{ motion = "dTx", result = "abc_xyz" },
}
for _, test in ipairs(tests) do
it("works with " .. test.motion, function()
vim.cmd("norm! " .. test.motion)
assert.same(test.result, get())
end)
end
for _, test in ipairs(tests) do
it("works with " .. test.motion .. " (flash)", function()
-- vim.api.nvim_feedkeys(test.motion, "mtx", false)
vim.cmd("norm " .. test.motion)
assert.same(test.result, get())
end)
end
local input = "abcd1abcd2abcd"
for _, motion in ipairs({ "f", "t", "F", "T" }) do
for col = 0, #input - 1 do
for count = -1, 3 do
count = count == -1 and "" or count
for _, char in ipairs({ "a", "b", "c", "d" }) do
local cmd = count .. "d" .. motion .. char
local pos = { 1, col }
it("works with " .. cmd .. " at " .. col, function()
set(input, pos)
vim.cmd("norm! " .. cmd)
local ret = get()
set(input, pos)
if Char.state then
Char.state:hide()
end
vim.cmd("norm " .. cmd)
assert.same(ret, get())
end)
end
end
end
end
end)

View File

@ -0,0 +1,38 @@
local Config = require("flash.config")
local assert = require("luassert")
describe("config", function()
before_each(function()
Config.setup()
end)
it("processes modes", function()
Config.setup({ modes = { foo = { bar = true } } })
assert.is_true(Config.modes.foo.bar)
assert.is_true(Config.get({ mode = "foo" }).bar)
end)
it("processes modes recursively", function()
Config.setup({
modes = {
foo = { mode = "bar" },
bar = { field = true, mode = "foo" },
},
})
assert.is_true(Config.get({ mode = "foo" }).field)
assert.is_true(Config.get({ mode = "bar" }).field)
end)
it("processes modes recursively in correct order", function()
Config.setup({
modes = {
a = { mode = "b", v = "a" },
b = { mode = "c", v = "b" },
c = { v = "c" },
},
})
assert.same("d", Config.get({ mode = "a", v = "d" }).v)
assert.same("a", Config.get({ mode = "a" }).v)
assert.same("b", Config.get({ mode = "b" }).v)
end)
end)

View File

@ -0,0 +1,36 @@
local M = {}
function M.root(root)
local f = debug.getinfo(1, "S").source:sub(2)
return vim.fn.fnamemodify(f, ":p:h:h") .. "/" .. (root or "")
end
---@param plugin string
function M.load(plugin)
local name = plugin:match(".*/(.*)")
local package_root = M.root(".tests/site/pack/deps/start/")
if not vim.loop.fs_stat(package_root .. name) then
print("Installing " .. plugin)
vim.fn.mkdir(package_root, "p")
vim.fn.system({
"git",
"clone",
"--depth=1",
"https://github.com/" .. plugin .. ".git",
package_root .. "/" .. name,
})
end
end
function M.setup()
vim.cmd([[set runtimepath=$VIMRUNTIME]])
vim.opt.runtimepath:append(M.root())
vim.opt.packpath = { M.root(".tests/site") }
M.load("nvim-lua/plenary.nvim")
vim.env.XDG_CONFIG_HOME = M.root(".tests/config")
vim.env.XDG_DATA_HOME = M.root(".tests/data")
vim.env.XDG_STATE_HOME = M.root(".tests/state")
vim.env.XDG_CACHE_HOME = M.root(".tests/cache")
end
M.setup()

View File

@ -0,0 +1,3 @@
#!/nix/store/306znyj77fv49kwnkpxmb0j2znqpa8bj-bash-5.2p26/bin/sh
nvim --headless -u tests/init.lua -c "PlenaryBustedDirectory tests {minimal_init = 'tests//init.lua', sequential = true}"

View File

@ -0,0 +1,51 @@
local Search = require("flash.search")
local State = require("flash.state")
describe("search", function()
before_each(function()
vim.opt.ignorecase = true
vim.opt.smartcase = true
vim.api.nvim_buf_set_lines(1, 0, -1, false, {})
end)
---@param opts? Flash.State.Config | string
local function get_search(opts)
if type(opts) == "string" then
opts = { pattern = opts }
end
local state = State.new(opts)
local win = vim.api.nvim_get_current_win()
return Search.new(win, state)
end
local function set(text, pos)
local lines = vim.split(vim.trim(text), "\n")
lines = vim.tbl_map(function(line)
return vim.trim(line)
end, lines)
vim.api.nvim_buf_set_lines(1, 0, -1, false, lines)
vim.api.nvim_win_set_cursor(0, pos or { 1, 0 })
end
it("finds matches", function()
set([[
foobar
line1
line2
]])
local search = get_search("line")
local matches = search:get()
assert.same("\\Vline", search.state.pattern.search)
assert.same({
{ win = 1000, pos = { 2, 0 }, end_pos = { 2, 3 } },
{ win = 1000, pos = { 3, 0 }, end_pos = { 3, 3 } },
}, matches)
assert.same({ win = 1000, pos = { 2, 0 }, end_pos = { 2, 3 } }, search:find())
assert.same({ win = 1000, pos = { 3, 0 }, end_pos = { 3, 3 } }, search:find({ count = 2 }))
assert.same(
{ win = 1000, pos = { 3, 0 }, end_pos = { 3, 3 } },
search:find({ forward = false })
)
end)
end)

View File

@ -0,0 +1,85 @@
local Jump = require("flash.jump")
local Pos = require("flash.search.pos")
local State = require("flash.state")
local assert = require("luassert")
describe("jump", function()
local function set(text, pos)
local lines = vim.split(vim.trim(text), "\n")
lines = vim.tbl_map(function(line)
return vim.trim(line)
end, lines)
if vim.fn.mode() == "v" then
vim.cmd("normal! v")
end
vim.api.nvim_buf_set_lines(1, 0, -1, false, lines)
vim.api.nvim_win_set_cursor(0, pos or { 1, 0 })
end
---@param match Flash.Match
---@param opts? Flash.State.Config
local function jump(match, opts)
match.win = vim.api.nvim_get_current_win()
local state = State.new(opts)
Jump.jump(match, state)
end
before_each(function()
vim.opt.ignorecase = true
vim.opt.smartcase = true
vim.api.nvim_buf_set_lines(0, 0, -1, false, {})
set([[
line1 foo
line2 foo
line3 foo
]])
end)
it("jumps to start", function()
local match = {
pos = Pos({ 1, 6 }),
end_pos = Pos({ 1, 8 }),
}
jump(match)
assert.same({ 1, 6 }, vim.api.nvim_win_get_cursor(0))
end)
it("selects to start", function()
assert.same({ 1, 0 }, vim.api.nvim_win_get_cursor(0))
local match = {
pos = Pos({ 1, 6 }),
end_pos = Pos({ 1, 8 }),
}
assert.same("n", vim.fn.mode())
vim.cmd("normal! v")
jump(match, { jump = { pos = "start", inclusive = false } })
assert.same("v", vim.fn.mode())
vim.cmd("normal! v")
assert.same({ 1, 6 }, vim.api.nvim_win_get_cursor(0))
assert.same({ 1, 0 }, vim.api.nvim_buf_get_mark(0, "<"))
assert.same({ 1, 6 }, vim.api.nvim_buf_get_mark(0, ">"))
end)
-- it("yanks to start", function()
-- assert.same({ 1, 0 }, vim.api.nvim_win_get_cursor(0))
-- local match = {
-- pos = Pos({ 1, 6 }),
-- end_pos = Pos({ 1, 8 }),
-- }
--
-- assert.same("n", vim.fn.mode())
-- -- vim.cmd("normal! y")
-- vim.api.nvim_feedkeys("y", "n", false)
-- assert.same("no", vim.fn.mode(true))
-- jump(match, { jump = { pos = "start", inclusive = false } })
-- assert.same("n", vim.fn.mode())
--
-- vim.print(vim.fn.getmarklist(vim.api.nvim_get_current_buf()))
--
-- assert.same({ 1, 6 }, vim.api.nvim_win_get_cursor(0))
-- assert.same({ 1, 0 }, vim.api.nvim_buf_get_mark(0, "["))
-- assert.same({ 1, 6 }, vim.api.nvim_buf_get_mark(0, "]"))
-- end)
end)

View File

@ -0,0 +1,103 @@
local Labeler = require("flash.labeler")
local Search = require("flash.search")
local State = require("flash.state")
local assert = require("luassert")
describe("labeler", function()
local function set(text, pos)
local lines = vim.split(vim.trim(text), "\n")
lines = vim.tbl_map(function(line)
return vim.trim(line)
end, lines)
vim.api.nvim_buf_set_lines(1, 0, -1, false, lines)
vim.api.nvim_win_set_cursor(0, pos or { 1, 0 })
end
local function search(pattern)
local state = State.new({ pattern = pattern, search = {
mode = "search",
} })
return Labeler.new(state)
end
before_each(function()
vim.opt.ignorecase = true
vim.opt.smartcase = true
end)
it("skips labels", function()
set([[
foo foo
bar
barfoo
]])
local labels = search("bar"):skip(1000, { "a", "b", "c", "f" })
assert.same({ "a", "b", "c" }, labels)
end)
it("skips all labels for an empty pattern", function()
set([[
test pattern
]])
local labels = search(""):skip(1000, { "a", "b", "c", "t" })
assert.same({}, labels)
end)
it("skips all labels for an invalid pattern", function()
set([[
invalid pattern
]])
local labels = search("[i"):skip(1000, { "a", "b", "i", "v" })
assert.same({}, labels)
end)
it("skips all labels when pattern ends with unescaped backslash", function()
set([[
pattern with backslash\
]])
local labels = search("backslash\\"):skip(1000, { "a", "b", "s", "\\" })
assert.same({}, labels)
end)
it("skips label that matches pattern", function()
set([[
pattern withc
]])
local labels = search("with"):skip(1000, { "a", "b", "c", "p", "w" })
assert.same({ "a", "b", "p", "w" }, labels)
end)
it("considers ignorecase when skipping labels", function()
set([[
pattern withC
]])
vim.opt.ignorecase = true
local labels = search("with"):skip(1000, { "a", "b", "C", "p", "w" })
assert.same({ "a", "b", "p", "w" }, labels)
end)
it("considers ignorecase3 when skipping labels", function()
set([[
pattern withC
]])
vim.opt.ignorecase = true
local labels = search("with"):skip(1000, { "a", "b", "c", "p", "w" })
assert.same({ "a", "b", "p", "w" }, labels)
end)
it("considers ignorecase2 when skipping labels", function()
set([[
pattern withc
]])
vim.opt.ignorecase = true
local labels = search("with"):skip(1000, { "a", "b", "C", "p", "w" })
assert.same({ "a", "b", "p", "w" }, labels)
end)
it("skips all labels when pattern is an incomplete regex", function()
set([[
pattern with incomplete regex (
]])
local labels = search("regex \\("):skip(1000, { "a", "b", "i", "r", "(" })
assert.same({}, labels)
end)
end)

View File

@ -0,0 +1,184 @@
local Matcher = require("flash.search.matcher")
local assert = require("luassert")
describe("search", function()
local matcher = Matcher.new(1000)
local matches = {
{ win = 1000, pos = { 1, 0 }, end_pos = { 1, 2 } },
{ win = 1000, pos = { 1, 7 }, end_pos = { 1, 9 } },
{ win = 1000, pos = { 3, 4 }, end_pos = { 3, 6 } },
}
matcher:set(matches)
it("sets matches", function()
assert.same(matches, matcher:get())
end)
it("finds backward from after end", function()
assert.same(
matches[3],
matcher:find({
forward = false,
pos = { 4, 6 },
wrap = false,
})
)
end)
it("handles count = 0", function()
assert.same(
matches[2],
matcher:find({
pos = { 1, 7 },
count = 0,
})
)
assert.is_nil(matcher:find({
pos = { 2, 7 },
count = 0,
}))
end)
it("returns forward matches", function()
assert.same(
{ matches[3] },
matcher:get({
from = { 2, 6 },
})
)
end)
it("returns forward matches", function()
assert.same(
{ matches[3] },
matcher:get({
from = { 3, 4 },
})
)
end)
it("returns backward matches", function()
assert.same(
{ matches[1] },
matcher:get({
to = { 1, 6 },
})
)
end)
it("returns backward matches", function()
assert.same(
{ matches[1] },
matcher:get({
to = { 1, 0 },
})
)
end)
it("finds matcher", function()
assert.same({ win = 1000, pos = { 1, 7 }, end_pos = { 1, 9 } }, matcher:find())
assert.same({ win = 1000, pos = { 3, 4 }, end_pos = { 3, 6 } }, matcher:find({ count = 2 }))
assert.same(
{ win = 1000, pos = { 3, 4 }, end_pos = { 3, 6 } },
matcher:find({ forward = false })
)
assert.same(
{ win = 1000, pos = { 1, 7 }, end_pos = { 1, 9 } },
matcher:find({
forward = false,
pos = { 2, 7 },
})
)
assert.same(
{ win = 1000, pos = { 1, 7 }, end_pos = { 1, 9 } },
matcher:find({
forward = false,
pos = { 3, 4 },
})
)
end)
it("sorts matches", function()
local m = Matcher.new(1000)
local mm = {
{ pos = { 3, 4 }, end_pos = { 3, 6 } },
{ pos = { 1, 0 }, end_pos = { 1, 2 } },
{ pos = { 1, 7 }, end_pos = { 1, 9 } },
}
m:set(mm)
assert.same({
{ win = 1000, pos = { 1, 0 }, end_pos = { 1, 2 } },
{ win = 1000, pos = { 1, 7 }, end_pos = { 1, 9 } },
{ win = 1000, pos = { 3, 4 }, end_pos = { 3, 6 } },
}, m:get())
end)
it("finds forward skipping match at current position", function()
assert.same(
matches[2],
matcher:find({
forward = true,
pos = { 1, 0 },
wrap = false,
})
)
end)
it("finds backward skipping match at current position", function()
assert.same(
matches[2],
matcher:find({
forward = false,
pos = { 3, 4 },
wrap = true,
})
)
end)
it("finds forward from a non-match position", function()
assert.same(
matches[2],
matcher:find({
forward = true,
pos = { 1, 3 },
wrap = false,
})
)
end)
it("finds backward from a non-match position", function()
assert.same(
matches[2],
matcher:find({
forward = false,
pos = { 3, 2 },
wrap = true,
})
)
end)
it("returns nil when wrapping is disabled and no match is found forward", function()
assert.is_nil(matcher:find({
forward = true,
pos = { 4, 0 },
wrap = false,
}))
end)
it("returns nil when wrapping is disabled and no match is found backward", function()
assert.is_nil(matcher:find({
forward = false,
pos = { 0, 0 },
wrap = false,
}))
end)
it("can handle multiple matches on the same pos", function()
local mm = Matcher.new(1000)
mm:set({
{ win = 1000, pos = { 1, 0 }, end_pos = { 1, 1 } },
{ win = 1000, pos = { 1, 0 }, end_pos = { 1, 2 } },
{ win = 1000, pos = { 1, 0 }, end_pos = { 1, 3 } },
})
-- assert.same()
end)
end)

View File

@ -0,0 +1,43 @@
--# selene: allow(incorrect_standard_library_use)
local Pos = require("flash.search.pos")
describe("pos", function()
it("handles new", function()
assert.same(Pos({ row = 1, col = 2 }), { 1, 2 })
assert.same(Pos({ 1, 2 }), { 1, 2 })
end)
it("handles cmp", function()
assert(Pos({ 1, 2 }) < Pos({ 2, 2 }))
assert(Pos({ 1, 2 }) < Pos({ 1, 3 }))
assert(Pos({ 1, 2 }) <= Pos({ 1, 2 }))
assert(Pos({ 1, 2 }) <= Pos({ 2, 2 }))
assert(Pos({ 1, 2 }) <= Pos({ 1, 3 }))
assert(Pos({ 1, 2 }) == Pos({ 1, 2 }))
assert(Pos({ 1, 2 }) == Pos({ 1, 2 }))
assert(Pos({ 1, 2 }) ~= Pos({ 2, 2 }))
assert(Pos({ 1, 2 }) <= Pos({ 2, 2 }))
assert(Pos({ 1, 2 }) >= Pos({ 1, 2 }))
assert(Pos({ 1, 2 }) >= Pos({ 1, 1 }))
end)
it("handles add", function()
assert.same(Pos({ 1, 2 }) + Pos({ 1, 2 }), { 2, 4 })
assert.same(Pos({ 1, 2 }) + { 1, 2 }, { 2, 4 })
assert.same(Pos({ 1, 2 }) + { row = 1, col = 2 }, { 2, 4 })
end)
it("handles sub", function()
assert.same(Pos({ 1, 2 }) - Pos({ 1, 2 }), { 0, 0 })
assert.same(Pos({ 1, 2 }) - { 1, 2 }, { 0, 0 })
assert.same(Pos({ 1, 2 }) - { row = 1, col = 2 }, { 0, 0 })
end)
it("handles tostring", function()
assert.same(tostring(Pos({ 1, 2 })), "[1, 2]")
end)
it("handles id", function()
assert.same(Pos({ 1, 2 }):id(123), "123:1:2")
end)
end)

View File

@ -0,0 +1,182 @@
local Search = require("flash.search")
local State = require("flash.state")
local assert = require("luassert")
describe("search", function()
local function set(text, pos)
local lines = vim.split(vim.trim(text), "\n")
lines = vim.tbl_map(function(line)
return vim.trim(line)
end, lines)
vim.api.nvim_buf_set_lines(1, 0, -1, false, lines)
vim.api.nvim_win_set_cursor(0, pos or { 1, 0 })
end
before_each(function()
vim.opt.ignorecase = true
vim.opt.smartcase = true
vim.api.nvim_buf_set_lines(1, 0, -1, false, {})
set([[
foo foo
bar
barfoo
]])
end)
local state = State.new({ pattern = "foo" })
local search = Search.new(1000, state)
local matches = {
{ win = 1000, pos = { 1, 0 }, end_pos = { 1, 2 } },
{ win = 1000, pos = { 1, 4 }, end_pos = { 1, 6 } },
{ win = 1000, pos = { 3, 3 }, end_pos = { 3, 5 } },
}
it("sets matches", function()
assert.same(matches, search:get())
end)
it("finds backward from after end", function()
assert.same(
matches[3],
search:find({
forward = false,
pos = { 4, 6 },
wrap = false,
})
)
end)
it("handles count = 0", function()
assert.same(
matches[2],
search:find({
pos = { 1, 4 },
count = 0,
})
)
assert.is_nil(search:find({
pos = { 2, 7 },
count = 0,
}))
end)
it("returns forward matches", function()
assert.same(
{ matches[3] },
search:get({
from = { 2, 6 },
})
)
end)
it("returns forward matches", function()
assert.same(
{ matches[3] },
search:get({
from = { 3, 3 },
})
)
end)
it("returns backward matches", function()
assert.same(
{ matches[1] },
search:get({
to = { 1, 3 },
})
)
end)
it("returns backward matches at pos", function()
assert.same(
{ matches[1] },
search:get({
to = { 1, 0 },
})
)
end)
it("finds matcher", function()
assert.same({ win = 1000, pos = { 1, 4 }, end_pos = { 1, 6 } }, search:find())
assert.same({ win = 1000, pos = { 3, 3 }, end_pos = { 3, 5 } }, search:find({ count = 2 }))
assert.same(
{ win = 1000, pos = { 3, 3 }, end_pos = { 3, 5 } },
search:find({ forward = false })
)
assert.same(
{ win = 1000, pos = { 1, 4 }, end_pos = { 1, 6 } },
search:find({
forward = false,
pos = { 2, 7 },
})
)
assert.same(
{ win = 1000, pos = { 1, 4 }, end_pos = { 1, 6 } },
search:find({
forward = false,
pos = { 3, 2 },
})
)
end)
it("finds forward skipping match at current position", function()
assert.same(
matches[2],
search:find({
forward = true,
pos = { 1, 0 },
wrap = false,
})
)
end)
it("finds backward skipping match at current position", function()
assert.same(
matches[2],
search:find({
forward = false,
pos = { 3, 3 },
wrap = true,
})
)
end)
it("finds forward from a non-match position", function()
assert.same(
matches[2],
search:find({
forward = true,
pos = { 1, 3 },
wrap = false,
})
)
end)
it("finds backward from a non-match position", function()
assert.same(
matches[2],
search:find({
forward = false,
pos = { 3, 2 },
wrap = true,
})
)
end)
it("returns nil when wrapping is disabled and no match is found forward", function()
assert.is_nil(search:find({
forward = true,
pos = { 4, 0 },
wrap = false,
}))
end)
it("returns nil when wrapping is disabled and no match is found backward", function()
assert.is_nil(search:find({
forward = false,
pos = { 1, 0 },
wrap = false,
}))
end)
end)

View File

@ -0,0 +1,190 @@
local Search = require("flash.search")
local State = require("flash.state")
describe("search.view", function()
before_each(function()
vim.opt.ignorecase = true
vim.opt.smartcase = true
vim.api.nvim_buf_set_lines(1, 0, -1, false, {})
end)
local function get_matches(pattern)
local state = State.new({ pattern = pattern, search = {
mode = "search",
} })
local win = vim.api.nvim_get_current_win()
local search = Search.new(win, state)
return search:get()
end
local function set(text, pos)
local lines = vim.split(vim.trim(text), "\n")
lines = vim.tbl_map(function(line)
return vim.trim(line)
end, lines)
vim.api.nvim_buf_set_lines(1, 0, -1, false, lines)
vim.api.nvim_win_set_cursor(0, pos or { 1, 0 })
end
it("finds matches", function()
set([[
foobar
line1
line2
]])
local matches = get_matches("line")
assert.same({
{ win = 1000, pos = { 2, 0 }, end_pos = { 2, 3 } },
{ win = 1000, pos = { 3, 0 }, end_pos = { 3, 3 } },
}, matches)
end)
it("finds multi matches on same line", function()
set([[
foobar foobar
line1
lineFoo
]])
local matches = get_matches("foo")
assert.same({
{ win = 1000, pos = { 1, 0 }, end_pos = { 1, 2 } },
{ win = 1000, pos = { 1, 7 }, end_pos = { 1, 9 } },
{ win = 1000, pos = { 3, 4 }, end_pos = { 3, 6 } },
}, matches)
end)
it("deals with case", function()
set([[
foobar
Line1
line2
]])
local matches = get_matches("line")
assert.same({
{ win = 1000, pos = { 2, 0 }, end_pos = { 2, 3 } },
{ win = 1000, pos = { 3, 0 }, end_pos = { 3, 3 } },
}, matches)
end)
it("deals with smartcase", function()
set([[
foobar
Line1
line2
]])
local matches = get_matches("Line")
assert.same({
{ win = 1000, pos = { 2, 0 }, end_pos = { 2, 3 } },
}, matches)
end)
it("finds matches on each line", function()
set([[
line1
line2
line3
]])
local matches = get_matches("line")
assert.same({
{ win = 1000, pos = { 1, 0 }, end_pos = { 1, 3 } },
{ win = 1000, pos = { 2, 0 }, end_pos = { 2, 3 } },
{ win = 1000, pos = { 3, 0 }, end_pos = { 3, 3 } },
}, matches)
end)
it("handles '\\Vi\\zs\\.'", function()
set([[
line1
line2
line3
]])
local matches = get_matches([[\Vi\zs\m.]])
assert.same({
{ win = 1000, pos = { 1, 2 }, end_pos = { 1, 2 } },
{ win = 1000, pos = { 2, 2 }, end_pos = { 2, 2 } },
{ win = 1000, pos = { 3, 2 }, end_pos = { 3, 2 } },
}, matches)
end)
it("handles ^", function()
set([[
foobar
line1
line2
]])
local matches = get_matches("^")
assert.same({
{ win = 1000, pos = { 1, 0 }, end_pos = { 1, 0 } },
{ win = 1000, pos = { 2, 0 }, end_pos = { 2, 0 } },
{ win = 1000, pos = { 3, 0 }, end_pos = { 3, 0 } },
}, matches)
end)
it("handles ^", function()
set([[
foobar
line1
line2
]])
local matches = get_matches("^")
assert.same({
{ win = 1000, pos = { 1, 0 }, end_pos = { 1, 0 } },
{ win = 1000, pos = { 2, 0 }, end_pos = { 2, 0 } },
{ win = 1000, pos = { 3, 0 }, end_pos = { 3, 0 } },
}, matches)
end)
it("handles ^.\\?", function()
set([[
foobar
line1
line2
]])
local matches = get_matches("^.\\?")
assert.same({
{ win = 1000, pos = { 1, 0 }, end_pos = { 1, 0 } },
{ win = 1000, pos = { 2, 0 }, end_pos = { 2, 0 } },
{ win = 1000, pos = { 3, 0 }, end_pos = { 3, 0 } },
}, matches)
end)
it("handles ^l", function()
set([[
foobar
line1
line2
]])
local matches = get_matches("^l")
assert.same({
{ win = 1000, pos = { 2, 0 }, end_pos = { 2, 0 } },
{ win = 1000, pos = { 3, 0 }, end_pos = { 3, 0 } },
}, matches)
end)
it("handles wrapping", function()
set(
[[
foo
line1
foo
]],
{ 3, 0 }
)
local matches = get_matches("foo")
assert.same({
{ win = 1000, pos = { 1, 0 }, end_pos = { 1, 2 } },
{ win = 1000, pos = { 3, 0 }, end_pos = { 3, 2 } },
}, matches)
end)
end)

View File

@ -0,0 +1,11 @@
local State = require("flash.state")
local assert = require("luassert")
describe("unicode", function()
local labels = "😅😀🏇🐎🐴🐵🐒"
it("splits labels", function()
local state = State.new({ labels = labels })
assert.same({ "😅", "😀", "🏇", "🐎", "🐴", "🐵", "🐒" }, state:labels())
end)
end)

View File

@ -0,0 +1,49 @@
[selene]
base = "lua51"
name = "vim"
[vim]
any = true
[jit]
any = true
[[describe.args]]
type = "string"
[[describe.args]]
type = "function"
[[it.args]]
type = "string"
[[it.args]]
type = "function"
[[before_each.args]]
type = "function"
[[after_each.args]]
type = "function"
[assert.is_not]
any = true
[[assert.equals.args]]
type = "any"
[[assert.equals.args]]
type = "any"
[[assert.equals.args]]
type = "any"
required = false
[[assert.same.args]]
type = "any"
[[assert.same.args]]
type = "any"
[[assert.truthy.args]]
type = "any"
[[assert.spy.args]]
type = "any"
[[assert.stub.args]]
type = "any"