├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── illuminate.yml ├── .gitignore ├── LICENSE ├── README.md ├── autoload └── illuminate.vim ├── doc └── illuminate.txt ├── foo.txt ├── lua ├── illuminate.lua └── illuminate │ ├── config.lua │ ├── engine.lua │ ├── goto.lua │ ├── highlight.lua │ ├── providers │ ├── lsp.lua │ ├── regex.lua │ └── treesitter.lua │ ├── reference.lua │ ├── textobj.lua │ └── util.lua └── plugin └── illuminate.vim /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 4 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior (include minimal `init.vim` or `.vimrc`): 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Note** Omitting a minimal `init.vim`/`init.lua`/`.vimrc` will likely result in the issue being closed without explanation. 18 | 19 | **Output from `:IlluminateDebug`** 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/workflows/illuminate.yml: -------------------------------------------------------------------------------- 1 | name: panvimdoc 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | 8 | jobs: 9 | docs: 10 | runs-on: ubuntu-latest 11 | name: pandoc to vimdoc 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: illuminate 15 | uses: kdheepak/panvimdoc@main 16 | with: 17 | vimdoc: illuminate 18 | pandoc: "README.md" 19 | toc: true 20 | version: "NVIM v0.8.0" 21 | - name: Check for Changes 22 | id: changes 23 | uses: UnicornGlobal/has-changes-action@v1.0.11 24 | with: 25 | path: 'doc/' 26 | - uses: stefanzweifel/git-auto-commit-action@v4 27 | if: steps.changes.outputs.has_changes == 'true' 28 | with: 29 | commit_message: 'Auto generate docs' 30 | branch: ${{ github.head_ref }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc/tags 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Adam Regasz-Rethy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Neovim plugin for automatically highlighting other uses of the word under the cursor using either LSP, Tree-sitter, or regex matching. 4 | 5 | https://github.com/user-attachments/assets/fc1487cf-aa19-4238-af23-ab1ac2f5744a 6 | 7 | # Quickstart 8 | 9 | Just install the plugin and things will work *just work*, no configuration needed. 10 | 11 | You'll also get `` and `` as keymaps to move between references and `` as a textobject for the reference illuminated under the cursor. 12 | 13 | *Note: Vim users should refer to the [docs](https://github.com/RRethy/vim-illuminate?tab=readme-ov-file#vim-users) for the Vimscript implementation of this plugin.* 14 | 15 | # Configuration 16 | 17 | ```lua 18 | -- default configuration 19 | require('illuminate').configure({ 20 | -- providers: provider used to get references in the buffer, ordered by priority 21 | providers = { 22 | 'lsp', 23 | 'treesitter', 24 | 'regex', 25 | }, 26 | -- delay: delay in milliseconds 27 | delay = 100, 28 | -- filetype_overrides: filetype specific overrides. 29 | -- The keys are strings to represent the filetype while the values are tables that 30 | -- supports the same keys passed to .configure except for filetypes_denylist and filetypes_allowlist 31 | filetype_overrides = {}, 32 | -- filetypes_denylist: filetypes to not illuminate, this overrides filetypes_allowlist 33 | filetypes_denylist = { 34 | 'dirbuf', 35 | 'dirvish', 36 | 'fugitive', 37 | }, 38 | -- filetypes_allowlist: filetypes to illuminate, this is overridden by filetypes_denylist 39 | -- You must set filetypes_denylist = {} to override the defaults to allow filetypes_allowlist to take effect 40 | filetypes_allowlist = {}, 41 | -- modes_denylist: modes to not illuminate, this overrides modes_allowlist 42 | -- See `:help mode()` for possible values 43 | modes_denylist = {}, 44 | -- modes_allowlist: modes to illuminate, this is overridden by modes_denylist 45 | -- See `:help mode()` for possible values 46 | modes_allowlist = {}, 47 | -- providers_regex_syntax_denylist: syntax to not illuminate, this overrides providers_regex_syntax_allowlist 48 | -- Only applies to the 'regex' provider 49 | -- Use :echom synIDattr(synIDtrans(synID(line('.'), col('.'), 1)), 'name') 50 | providers_regex_syntax_denylist = {}, 51 | -- providers_regex_syntax_allowlist: syntax to illuminate, this is overridden by providers_regex_syntax_denylist 52 | -- Only applies to the 'regex' provider 53 | -- Use :echom synIDattr(synIDtrans(synID(line('.'), col('.'), 1)), 'name') 54 | providers_regex_syntax_allowlist = {}, 55 | -- under_cursor: whether or not to illuminate under the cursor 56 | under_cursor = true, 57 | -- large_file_cutoff: number of lines at which to use large_file_config 58 | -- The `under_cursor` option is disabled when this cutoff is hit 59 | large_file_cutoff = 10000, 60 | -- large_file_config: config to use for large files (based on large_file_cutoff). 61 | -- Supports the same keys passed to .configure 62 | -- If nil, vim-illuminate will be disabled for large files. 63 | large_file_overrides = nil, 64 | -- min_count_to_highlight: minimum number of matches required to perform highlighting 65 | min_count_to_highlight = 1, 66 | -- should_enable: a callback that overrides all other settings to 67 | -- enable/disable illumination. This will be called a lot so don't do 68 | -- anything expensive in it. 69 | should_enable = function(bufnr) return true end, 70 | -- case_insensitive_regex: sets regex case sensitivity 71 | case_insensitive_regex = false, 72 | -- disable_keymaps: disable default keymaps 73 | disable_keymaps = false, 74 | }) 75 | ``` 76 | 77 | # Highlight Groups 78 | 79 | #### IlluminatedWordText 80 | 81 | Default highlight group used for references if no kind information is available. 82 | 83 | ```vim 84 | hi def IlluminatedWordText gui=underline cterm=underline 85 | ``` 86 | 87 | #### IlluminatedWordRead 88 | 89 | Highlight group used for references of kind read. 90 | 91 | ```vim 92 | hi def IlluminatedWordRead gui=underline cterm=underline 93 | ``` 94 | 95 | #### IlluminatedWordWrite 96 | 97 | Highlight group used for references of kind write. 98 | 99 | ```vim 100 | hi def IlluminatedWordWrite gui=underline cterm=underline 101 | ``` 102 | 103 | # Commands 104 | 105 | #### :IlluminatePause 106 | 107 | Globally pause vim-illuminate. 108 | 109 | #### :IlluminateResume 110 | 111 | Globally resume vim-illuminate. 112 | 113 | #### :IlluminateToggle 114 | 115 | Globally toggle the pause/resume for vim-illuminate. 116 | 117 | #### :IlluminatePauseBuf 118 | 119 | Buffer-local pause of vim-illuminate. 120 | 121 | #### :IlluminateResumeBuf 122 | 123 | Buffer-local resume of vim-illuminate. 124 | 125 | #### :IlluminateToggleBuf 126 | 127 | Buffer-local toggle of the pause/resume for vim-illuminate. 128 | 129 | # Functions 130 | 131 | #### require('illuminate').configure(config) 132 | 133 | Override the default configuration with `config` 134 | 135 | #### require('illuminate').pause() 136 | 137 | Globally pause vim-illuminate. 138 | 139 | #### require('illuminate').resume() 140 | 141 | Globally resume vim-illuminate. 142 | 143 | #### require('illuminate').toggle() 144 | 145 | Globally toggle the pause/resume for vim-illuminate. 146 | 147 | #### require('illuminate').toggle_buf() 148 | 149 | Buffer-local toggle of the pause/resume for vim-illuminate. 150 | 151 | #### require('illuminate').pause_buf() 152 | 153 | Buffer-local pause of vim-illuminate. 154 | 155 | #### require('illuminate').resume_buf() 156 | 157 | Buffer-local resume of vim-illuminate. 158 | 159 | #### require('illuminate').freeze_buf() 160 | 161 | Freeze the illumination on the buffer, this won't clear the highlights. 162 | 163 | #### require('illuminate').unfreeze_buf() 164 | 165 | Unfreeze the illumination on the buffer. 166 | 167 | #### require('illuminate').toggle_freeze_buf() 168 | 169 | Toggle the frozen state of the buffer. 170 | 171 | #### require('illuminate').invisible_buf() 172 | 173 | Turn off the highlighting for the buffer, this won't stop the engine from running so you can still use `` and ``. 174 | 175 | #### require('illuminate').visible_buf() 176 | 177 | Turn on the highlighting for the buffer, this is only needed if you've previous called `require('illuminate').invisible_buf()`. 178 | 179 | #### require('illuminate').toggle_visibility_buf() 180 | 181 | Toggle the visibility of highlights in the buffer. 182 | 183 | #### require('illuminate').goto_next_reference(wrap) 184 | 185 | Move the cursor to the closest references after the cursor which it is not currently on. Wraps the buffer if on the last reference. 186 | 187 | Wraps the references unless `wrap` is false (defaults to **'wrapscan'**). 188 | 189 | #### require('illuminate').goto_prev_reference(wrap) 190 | 191 | Move the cursor to the closest references before the cursor which it is not currently on. Wraps the buffer if on the first reference. 192 | 193 | Wraps the references unless `wrap` is false (defaults to **'wrapscan'**). 194 | 195 | #### require('illuminate').textobj_select() 196 | 197 | Selects the reference the cursor is currently on for use as a text-object. 198 | 199 | # Vim Users 200 | 201 | **Note:** This section is deprecated for Neovim users, Neovim users can use the newer version of the plugin. Neovim users can force this old version of the plugin by adding `let g:Illuminate_useDeprecated = 1` to their `init.vim`. 202 | 203 | Illuminate will delay before highlighting, this is not lag, it is to avoid the jarring experience of things illuminating too fast. This can be controlled with `g:Illuminate_delay` (which is default to 0 milliseconds): 204 | 205 | **Note**: Delay only works for Vim8 and Neovim. 206 | 207 | ```vim 208 | " Time in milliseconds (default 0) 209 | let g:Illuminate_delay = 0 210 | ``` 211 | Illuminate will by default highlight the word under the cursor to match the behaviour seen in Intellij and VSCode. However, to make it not highlight the word under the cursor, use the following: 212 | 213 | ```vim 214 | " Don't highlight word under cursor (default: 1) 215 | let g:Illuminate_highlightUnderCursor = 0 216 | ``` 217 | 218 | By default illuminate will highlight all words the cursor passes over, but for many languages, you will only want to highlight certain highlight-groups. 219 | You can determine the highlight-group of a symbol under your cursor with `:echo synIDattr(synID(line("."), col("."), 1), "name")`. 220 | 221 | You can define which highlight groups you want the illuminating to apply to. This can be done with a dict mapping a filetype to a list of highlight-groups in your vimrc such as: 222 | ```vim 223 | let g:Illuminate_ftHighlightGroups = { 224 | \ 'vim': ['vimVar', 'vimString', 'vimLineComment', 225 | \ 'vimFuncName', 'vimFunction', 'vimUserFunc', 'vimFunc'] 226 | \ } 227 | ``` 228 | 229 | A blacklist of highlight groups can also be setup by adding the suffix `:blacklist` to the filetype. However, if the whitelist for that filetype already exists, it will override the blacklist. 230 | ```vim 231 | let g:Illuminate_ftHighlightGroups = { 232 | \ 'vim:blacklist': ['vimVar', 'vimString', 'vimLineComment', 233 | \ 'vimFuncName', 'vimFunction', 'vimUserFunc', 'vimFunc'] 234 | \ } 235 | ``` 236 | 237 | illuminate can also be disabled for various filetypes using the following: 238 | ```vim 239 | let g:Illuminate_ftblacklist = ['nerdtree'] 240 | ``` 241 | 242 | Or you can enable it only for certain filetypes with: 243 | ```vim 244 | let g:Illuminate_ftwhitelist = ['vim', 'sh', 'python'] 245 | ``` 246 | 247 | By default the highlighting will be done with the highlight-group `CursorLine` since that is in my opinion the nicest. It can however be overridden using the following (use standard Vim highlighting syntax): 248 | Note: It must be in an autocmd to get around a weird Neovim behaviour. 249 | ```vim 250 | augroup illuminate_augroup 251 | autocmd! 252 | autocmd VimEnter * hi link illuminatedWord CursorLine 253 | augroup END 254 | 255 | augroup illuminate_augroup 256 | autocmd! 257 | autocmd VimEnter * hi illuminatedWord cterm=underline gui=underline 258 | augroup END 259 | ``` 260 | 261 | Lastly, you can also specify a specific highlight for the word under the cursor so it differs from all other matches using the following higlight group: 262 | ```vim 263 | augroup illuminate_augroup 264 | autocmd! 265 | autocmd VimEnter * hi illuminatedCurWord cterm=italic gui=italic 266 | augroup END 267 | ``` 268 | -------------------------------------------------------------------------------- /autoload/illuminate.vim: -------------------------------------------------------------------------------- 1 | " illuminate.vim - Vim plugin for selectively illuminating other uses of current word 2 | " Maintainer: Adam P. Regasz-Rethy (RRethy) 3 | " Version: 0.4 4 | 5 | let s:previous_match = '' 6 | let s:enabled = 1 7 | 8 | let g:Illuminate_delay = get(g:, 'Illuminate_delay', 0) 9 | let g:Illuminate_highlightUnderCursor = get(g:, 'Illuminate_highlightUnderCursor', 1) 10 | let g:Illuminate_highlightPriority = get(g:, 'Illuminate_highlightPriority', -1) 11 | 12 | fun! illuminate#on_cursor_moved() abort 13 | if !s:should_illuminate_file() 14 | return 15 | endif 16 | 17 | if s:previous_match !=# s:get_cur_word() 18 | call s:remove_illumination() 19 | elseif get(g:, 'Illuminate_highlightUnderCursor', 1) == 0 || hlexists('illuminatedCurWord') 20 | call s:remove_illumination() 21 | call s:illuminate() 22 | return 23 | else 24 | return 25 | endif 26 | 27 | " Any delay at or below 17 milliseconds gets counted as no delay 28 | if !has('timers') || g:Illuminate_delay <= 17 29 | call s:illuminate() 30 | return 31 | endif 32 | 33 | if exists('s:timer_id') && s:timer_id > -1 34 | call timer_stop(s:timer_id) 35 | endif 36 | 37 | let s:timer_id = timer_start(g:Illuminate_delay, function('s:illuminate')) 38 | endf 39 | 40 | fun! illuminate#on_leaving_autocmds() abort 41 | if s:should_illuminate_file() 42 | call s:remove_illumination() 43 | endif 44 | endf 45 | 46 | fun! illuminate#on_cursor_moved_i() abort 47 | if get(g:, 'Illuminate_insert_mode_highlight', 0) 48 | call illuminate#on_cursor_moved() 49 | endif 50 | endf 51 | 52 | fun! illuminate#on_insert_entered() abort 53 | if !get(g:, 'Illuminate_insert_mode_highlight', 0) && s:should_illuminate_file() 54 | call s:remove_illumination() 55 | endif 56 | endf 57 | 58 | fun! illuminate#toggle_illumination(bufonly) abort 59 | if a:bufonly 60 | let b:illuminate_enabled = get(b:, 'illuminate_enabled', s:enabled) 61 | if !b:illuminate_enabled 62 | call illuminate#enable_illumination(1) 63 | else 64 | call illuminate#disable_illumination(1) 65 | endif 66 | else 67 | if !s:enabled 68 | call illuminate#enable_illumination(0) 69 | else 70 | call illuminate#disable_illumination(0) 71 | endif 72 | endif 73 | endf 74 | 75 | fun! illuminate#disable_illumination(bufonly) abort 76 | if a:bufonly 77 | let b:illuminate_enabled = 0 78 | else 79 | let s:enabled = 0 80 | endif 81 | call s:remove_illumination() 82 | endf 83 | 84 | fun! illuminate#enable_illumination(bufonly) abort 85 | if a:bufonly 86 | let b:illuminate_enabled = 1 87 | else 88 | let s:enabled = 1 89 | endif 90 | if s:should_illuminate_file() 91 | call s:illuminate() 92 | endif 93 | endf 94 | 95 | fun! s:illuminate(...) abort 96 | if !get(b:, 'illuminate_enabled', s:enabled) 97 | return 98 | endif 99 | 100 | call s:remove_illumination() 101 | 102 | if s:should_illuminate_word() 103 | call s:match_word(s:get_cur_word()) 104 | endif 105 | let s:previous_match = s:get_cur_word() 106 | endf 107 | 108 | fun! s:match_word(word) abort 109 | if (a:word ==# '\<\>') 110 | return 111 | endif 112 | if g:Illuminate_highlightUnderCursor 113 | if hlexists('illuminatedCurWord') 114 | let w:match_id = matchadd('illuminatedWord', '\V\(\k\*\%#\k\*\)\@\!\&' . a:word, g:Illuminate_highlightPriority) 115 | let w:match_curword_id = matchadd('illuminatedCurWord', '\V\(\k\*\%#\k\*\)\&' . a:word, g:Illuminate_highlightPriority) 116 | else 117 | let w:match_id = matchadd('illuminatedWord', '\V' . a:word, g:Illuminate_highlightPriority) 118 | endif 119 | else 120 | let w:match_id = matchadd('illuminatedWord', '\V\(\k\*\%#\k\*\)\@\!\&' . a:word, g:Illuminate_highlightPriority) 121 | endif 122 | endf 123 | 124 | fun! s:get_cur_word() abort 125 | let line = getline('.') 126 | let col = col('.') - 1 127 | let left_part = strpart(line, 0, col + 1) 128 | let right_part = strpart(line, col, col('$')) 129 | let word = matchstr(left_part, '\k*$') . matchstr(right_part, '^\k*')[1:] 130 | 131 | return '\<' . escape(word, '/\') . '\>' 132 | endf 133 | 134 | fun! s:remove_illumination() abort 135 | if has('timers') && exists('s:timer_id') && s:timer_id > -1 136 | call timer_stop(s:timer_id) 137 | let s:timer_id = -1 138 | endif 139 | 140 | if exists('w:match_id') 141 | try 142 | call matchdelete(w:match_id) 143 | catch /\v(E803|E802)/ 144 | endtry 145 | endif 146 | 147 | if exists('w:match_curword_id') 148 | try 149 | call matchdelete(w:match_curword_id) 150 | catch /\v(E803|E802)/ 151 | endtry 152 | endif 153 | 154 | let s:previous_match = '' 155 | endf 156 | 157 | fun! s:should_illuminate_file() abort 158 | let g:Illuminate_ftblacklist = get(g:, 'Illuminate_ftblacklist', []) 159 | let g:Illuminate_ftwhitelist = get(g:, 'Illuminate_ftwhitelist', []) 160 | 161 | return !s:list_contains_pat(g:Illuminate_ftblacklist, &filetype) 162 | \ && (empty(g:Illuminate_ftwhitelist) || s:list_contains_pat(g:Illuminate_ftwhitelist, &filetype)) 163 | endf 164 | 165 | fun! s:should_illuminate_word() abort 166 | let ft_hl_groups = get(g:, 'Illuminate_ftHighlightGroups', {}) 167 | let hl_groups_whitelist = get(ft_hl_groups, &filetype, []) 168 | call extend(hl_groups_whitelist, get(ft_hl_groups, '*', [])) 169 | if empty(hl_groups_whitelist) 170 | let hl_groups_blacklist = get(ft_hl_groups, &filetype.':blacklist', []) 171 | call extend(hl_groups_blacklist, get(ft_hl_groups, '*:blacklist', [])) 172 | if empty(hl_groups_blacklist) 173 | return 1 174 | else 175 | return index(hl_groups_blacklist, synIDattr(synIDtrans(synID(line('.'), col('.'), 1)), 'name')) < 0 176 | \ && index(hl_groups_blacklist, synIDattr(synID(line('.'), col('.'), 1), 'name')) < 0 177 | endif 178 | endif 179 | 180 | return index(ft_hl_groups[&filetype], synIDattr(synIDtrans(synID(line('.'), col('.'), 1)), 'name')) >= 0 181 | \ || index(ft_hl_groups[&filetype], synIDattr(synID(line('.'), col('.'), 1), 'name')) >= 0 182 | endf 183 | 184 | fun! s:dict_has_key_pat(d, key) abort 185 | for [k, v] in items(a:d) 186 | if key =~# '^'.k.'$' 187 | return 1 188 | endif 189 | endfor 190 | return 0 191 | endfun 192 | 193 | fun! s:list_contains_pat(list, val) abort 194 | for pat in a:list 195 | if a:val =~# '^'.pat.'$' 196 | return 1 197 | endif 198 | endfor 199 | return 0 200 | endfun 201 | 202 | " vim: foldlevel=1 foldmethod=expr tabstop=2 softtabstop=2 shiftwidth=2 203 | -------------------------------------------------------------------------------- /doc/illuminate.txt: -------------------------------------------------------------------------------- 1 | *illuminate.txt* For NVIM v0.8.0 Last change: 2023 March 19 2 | 3 | ============================================================================== 4 | Table of Contents *illuminate-table-of-contents* 5 | 6 | 1. Overview |illuminate-overview| 7 | 2. Quickstart |illuminate-quickstart| 8 | 3. Configuration |illuminate-configuration| 9 | 4. Highlight Groups |illuminate-highlight-groups| 10 | 5. Commands |illuminate-commands| 11 | 6. Functions |illuminate-functions| 12 | 7. Vim Users |illuminate-vim-users| 13 | 14 | ============================================================================== 15 | 1. Overview *illuminate-overview* 16 | 17 | Vim plugin for automatically highlighting other uses of the word under the 18 | cursor using either LSP, Tree-sitter, or regex matching. 19 | 20 | 21 | ============================================================================== 22 | 2. Quickstart *illuminate-quickstart* 23 | 24 | Just install the plugin and things will work _just work_, no configuration 25 | needed. 26 | 27 | You’ll also get `` and `` as keymaps to move between references and 28 | `` as a textobject for the reference illuminated under the cursor. 29 | 30 | 31 | ============================================================================== 32 | 3. Configuration *illuminate-configuration* 33 | 34 | >lua 35 | -- default configuration 36 | require('illuminate').configure({ 37 | -- providers: provider used to get references in the buffer, ordered by priority 38 | providers = { 39 | 'lsp', 40 | 'treesitter', 41 | 'regex', 42 | }, 43 | -- delay: delay in milliseconds 44 | delay = 100, 45 | -- filetype_overrides: filetype specific overrides. 46 | -- The keys are strings to represent the filetype while the values are tables that 47 | -- supports the same keys passed to .configure except for filetypes_denylist and filetypes_allowlist 48 | filetype_overrides = {}, 49 | -- filetypes_denylist: filetypes to not illuminate, this overrides filetypes_allowlist 50 | filetypes_denylist = { 51 | 'dirvish', 52 | 'fugitive', 53 | }, 54 | -- filetypes_allowlist: filetypes to illuminate, this is overriden by filetypes_denylist 55 | filetypes_allowlist = {}, 56 | -- modes_denylist: modes to not illuminate, this overrides modes_allowlist 57 | -- See `:help mode()` for possible values 58 | modes_denylist = {}, 59 | -- modes_allowlist: modes to illuminate, this is overriden by modes_denylist 60 | -- See `:help mode()` for possible values 61 | modes_allowlist = {}, 62 | -- providers_regex_syntax_denylist: syntax to not illuminate, this overrides providers_regex_syntax_allowlist 63 | -- Only applies to the 'regex' provider 64 | -- Use :echom synIDattr(synIDtrans(synID(line('.'), col('.'), 1)), 'name') 65 | providers_regex_syntax_denylist = {}, 66 | -- providers_regex_syntax_allowlist: syntax to illuminate, this is overriden by providers_regex_syntax_denylist 67 | -- Only applies to the 'regex' provider 68 | -- Use :echom synIDattr(synIDtrans(synID(line('.'), col('.'), 1)), 'name') 69 | providers_regex_syntax_allowlist = {}, 70 | -- under_cursor: whether or not to illuminate under the cursor 71 | under_cursor = true, 72 | -- large_file_cutoff: number of lines at which to use large_file_config 73 | -- The `under_cursor` option is disabled when this cutoff is hit 74 | large_file_cutoff = nil, 75 | -- large_file_config: config to use for large files (based on large_file_cutoff). 76 | -- Supports the same keys passed to .configure 77 | -- If nil, vim-illuminate will be disabled for large files. 78 | large_file_overrides = nil, 79 | -- min_count_to_highlight: minimum number of matches required to perform highlighting 80 | min_count_to_highlight = 1, 81 | }) 82 | < 83 | 84 | 85 | ============================================================================== 86 | 4. Highlight Groups *illuminate-highlight-groups* 87 | 88 | 89 | ILLUMINATEDWORDTEXT 90 | 91 | Default highlight group used for references if no kind information is 92 | available. 93 | 94 | >vim 95 | hi def IlluminatedWordText gui=underline 96 | < 97 | 98 | 99 | ILLUMINATEDWORDREAD 100 | 101 | Highlight group used for references of kind read. 102 | 103 | >vim 104 | hi def IlluminatedWordRead gui=underline 105 | < 106 | 107 | 108 | ILLUMINATEDWORDWRITE 109 | 110 | Highlight group used for references of kind write. 111 | 112 | >vim 113 | hi def IlluminatedWordWrite gui=underline 114 | < 115 | 116 | 117 | ============================================================================== 118 | 5. Commands *illuminate-commands* 119 | 120 | 121 | :ILLUMINATEPAUSE 122 | 123 | Globally pause vim-illuminate. 124 | 125 | 126 | :ILLUMINATERESUME 127 | 128 | Globally resume vim-illuminate. 129 | 130 | 131 | :ILLUMINATETOGGLE 132 | 133 | Globally toggle the pause/resume for vim-illuminate. 134 | 135 | 136 | :ILLUMINATEPAUSEBUF 137 | 138 | Buffer-local pause of vim-illuminate. 139 | 140 | 141 | :ILLUMINATERESUMEBUF 142 | 143 | Buffer-local resume of vim-illuminate. 144 | 145 | 146 | :ILLUMINATETOGGLEBUF 147 | 148 | Buffer-local toggle of the pause/resume for vim-illuminate. 149 | 150 | 151 | ============================================================================== 152 | 6. Functions *illuminate-functions* 153 | 154 | 155 | REQUIRE(‘ILLUMINATE’).CONFIGURE(CONFIG) 156 | 157 | Override the default configuration with `config` 158 | 159 | 160 | REQUIRE(‘ILLUMINATE’).PAUSE() 161 | 162 | Globally pause vim-illuminate. 163 | 164 | 165 | REQUIRE(‘ILLUMINATE’).RESUME() 166 | 167 | Globally resume vim-illuminate. 168 | 169 | 170 | REQUIRE(‘ILLUMINATE’).TOGGLE() 171 | 172 | Globally toggle the pause/resume for vim-illuminate. 173 | 174 | 175 | REQUIRE(‘ILLUMINATE’).TOGGLE_BUF() 176 | 177 | Buffer-local toggle of the pause/resume for vim-illuminate. 178 | 179 | 180 | REQUIRE(‘ILLUMINATE’).PAUSE_BUF() 181 | 182 | Buffer-local pause of vim-illuminate. 183 | 184 | 185 | REQUIRE(‘ILLUMINATE’).RESUME_BUF() 186 | 187 | Buffer-local resume of vim-illuminate. 188 | 189 | 190 | REQUIRE(‘ILLUMINATE’).FREEZE_BUF() 191 | 192 | Freeze the illumination on the buffer, this won’t clear the highlights. 193 | 194 | 195 | REQUIRE(‘ILLUMINATE’).UNFREEZE_BUF() 196 | 197 | Unfreeze the illumination on the buffer. 198 | 199 | 200 | REQUIRE(‘ILLUMINATE’).TOGGLE_FREEZE_BUF() 201 | 202 | Toggle the frozen state of the buffer. 203 | 204 | 205 | REQUIRE(‘ILLUMINATE’).INVISIBLE_BUF() 206 | 207 | Turn off the highlighting for the buffer, this won’t stop the engine from 208 | running so you can still use `` and ``. 209 | 210 | 211 | REQUIRE(‘ILLUMINATE’).VISIBLE_BUF() 212 | 213 | Turn on the highlighting for the buffer, this is only needed if you’ve 214 | previous called `require('illuminate').invisible_buf()`. 215 | 216 | 217 | REQUIRE(‘ILLUMINATE’).TOGGLE_VISIBILITY_BUF() 218 | 219 | Toggle the visibility of highlights in the buffer. 220 | 221 | 222 | REQUIRE(‘ILLUMINATE’).GOTO_NEXT_REFERENCE(WRAP) 223 | 224 | Move the cursor to the closest references after the cursor which it is not 225 | currently on. Wraps the buffer if on the last reference. 226 | 227 | Wraps the references unless `wrap` is false (defaults to **‘wrapscan’**). 228 | 229 | 230 | REQUIRE(‘ILLUMINATE’).GOTO_PREV_REFERENCE(WRAP) 231 | 232 | Move the cursor to the closest references before the cursor which it is not 233 | currently on. Wraps the buffer if on the first reference. 234 | 235 | Wraps the references unless `wrap` is false (defaults to **‘wrapscan’**). 236 | 237 | 238 | REQUIRE(‘ILLUMINATE’).TEXTOBJ_SELECT() 239 | 240 | Selects the reference the cursor is currently on for use as a text-object. 241 | 242 | 243 | ============================================================================== 244 | 7. Vim Users *illuminate-vim-users* 245 | 246 | **Note:** This section is deprecated for Neovim users, Neovim users can use the 247 | newer version of the plugin. Neovim users can force this old version of the 248 | plugin by adding `let g:Illuminate_useDeprecated = 1` to their `init.vim`. 249 | 250 | Illuminate will delay before highlighting, this is not lag, it is to avoid the 251 | jarring experience of things illuminating too fast. This can be controlled with 252 | `g:Illuminate_delay` (which is default to 0 milliseconds): 253 | 254 | **Note**: Delay only works for Vim8 and Neovim. 255 | 256 | >vim 257 | " Time in milliseconds (default 0) 258 | let g:Illuminate_delay = 0 259 | < 260 | 261 | Illuminate will by default highlight the word under the cursor to match the 262 | behaviour seen in Intellij and VSCode. However, to make it not highlight the 263 | word under the cursor, use the following: 264 | 265 | >vim 266 | " Don't highlight word under cursor (default: 1) 267 | let g:Illuminate_highlightUnderCursor = 0 268 | < 269 | 270 | By default illuminate will highlight all words the cursor passes over, but for 271 | many languages, you will only want to highlight certain highlight-groups. You 272 | can determine the highlight-group of a symbol under your cursor with `:echo 273 | synIDattr(synID(line("."), col("."), 1), "name")`. 274 | 275 | You can define which highlight groups you want the illuminating to apply to. 276 | This can be done with a dict mapping a filetype to a list of highlight-groups 277 | in your vimrc such as: 278 | 279 | >vim 280 | let g:Illuminate_ftHighlightGroups = { 281 | \ 'vim': ['vimVar', 'vimString', 'vimLineComment', 282 | \ 'vimFuncName', 'vimFunction', 'vimUserFunc', 'vimFunc'] 283 | \ } 284 | < 285 | 286 | A blacklist of highlight groups can also be setup by adding the suffix 287 | `:blacklist` to the filetype. However, if the whitelist for that filetype 288 | already exists, it will override the blacklist. 289 | 290 | >vim 291 | let g:Illuminate_ftHighlightGroups = { 292 | \ 'vim:blacklist': ['vimVar', 'vimString', 'vimLineComment', 293 | \ 'vimFuncName', 'vimFunction', 'vimUserFunc', 'vimFunc'] 294 | \ } 295 | < 296 | 297 | illuminate can also be disabled for various filetypes using the following: 298 | 299 | >vim 300 | let g:Illuminate_ftblacklist = ['nerdtree'] 301 | < 302 | 303 | Or you can enable it only for certain filetypes with: 304 | 305 | >vim 306 | let g:Illuminate_ftwhitelist = ['vim', 'sh', 'python'] 307 | < 308 | 309 | By default the highlighting will be done with the highlight-group `CursorLine` 310 | since that is in my opinion the nicest. It can however be overridden using the 311 | following (use standard Vim highlighting syntax): Note: It must be in an 312 | autocmd to get around a weird Neovim behaviour. 313 | 314 | >vim 315 | augroup illuminate_augroup 316 | autocmd! 317 | autocmd VimEnter * hi link illuminatedWord CursorLine 318 | augroup END 319 | 320 | augroup illuminate_augroup 321 | autocmd! 322 | autocmd VimEnter * hi illuminatedWord cterm=underline gui=underline 323 | augroup END 324 | < 325 | 326 | Lastly, you can also specify a specific highlight for the word under the cursor 327 | so it differs from all other matches using the following higlight group: 328 | 329 | >vim 330 | augroup illuminate_augroup 331 | autocmd! 332 | autocmd VimEnter * hi illuminatedCurWord cterm=italic gui=italic 333 | augroup END 334 | < 335 | 336 | ============================================================================== 337 | 8. Links *illuminate-links* 338 | 339 | 1. *gif*: https://media.giphy.com/media/mSG0nwAHDt3Fl7WyoL/giphy.gif 340 | 341 | Generated by panvimdoc 342 | 343 | vim:tw=78:ts=8:noet:ft=help:norl: 344 | -------------------------------------------------------------------------------- /foo.txt: -------------------------------------------------------------------------------- 1 | hello 2 | 3 | hello 4 | 5 | hello 6 | 7 | hello 8 | 9 | hello 10 | 11 | hello 12 | 13 | hello 14 | hello 15 | hello 16 | hello 17 | hello world 18 | hello hello 19 | hello 20 | hello 21 | hello 22 | hello 23 | hello 24 | hello 25 | hello 26 | hello 27 | hello 28 | hello 29 | hello 30 | hello 31 | hello 32 | -------------------------------------------------------------------------------- /lua/illuminate.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local timers = {} 4 | local references = {} 5 | local paused_bufs = {} 6 | 7 | -- returns r1 < r2 based on start of range 8 | local function before_by_start(r1, r2) 9 | if r1['start'].line < r2['start'].line then return true end 10 | if r2['start'].line < r1['start'].line then return false end 11 | if r1['start'].character < r2['start'].character then return true end 12 | return false 13 | end 14 | 15 | -- returns r1 < r2 base on start and if they are disjoint 16 | local function before_disjoint(r1, r2) 17 | if r1['end'].line < r2['start'].line then return true end 18 | if r2['start'].line < r1['end'].line then return false end 19 | if r1['end'].character < r2['start'].character then return true end 20 | return false 21 | end 22 | 23 | -- check for cursor row in [start,end] 24 | -- check for cursor col in [start,end] 25 | -- While the end is technically exclusive based on the highlighting, we treat it as inclusive to match the server. 26 | local function point_in_range(point, range) 27 | if point.row == range['start']['line'] and point.col < range['start']['character'] then 28 | return false 29 | end 30 | if point.row == range['end']['line'] and point.col > range['end']['character'] then 31 | return false 32 | end 33 | return point.row >= range['start']['line'] and point.row <= range['end']['line'] 34 | end 35 | 36 | local function cursor_in_references(bufnr) 37 | if not references[bufnr] then 38 | return false 39 | end 40 | if vim.api.nvim_win_get_buf(0) ~= bufnr then 41 | return false 42 | end 43 | local crow, ccol = unpack(vim.api.nvim_win_get_cursor(0)) 44 | crow = crow - 1 -- reference ranges are (0,0)-indexed for (row,col) 45 | for _, reference in pairs(references[bufnr]) do 46 | local range = reference.range 47 | if point_in_range({ row = crow, col = ccol }, range) then 48 | return true 49 | end 50 | end 51 | return false 52 | end 53 | 54 | local function handle_document_highlight(result, bufnr, client_id) 55 | if not bufnr or not vim.api.nvim_buf_is_loaded(bufnr) then return end 56 | local btimer = timers[bufnr] 57 | if btimer then 58 | vim.loop.timer_stop(btimer) 59 | -- vim.loop.close(btimer) 60 | end 61 | if type(result) ~= 'table' then 62 | vim.lsp.util.buf_clear_references(bufnr) 63 | return 64 | end 65 | timers[bufnr] = vim.defer_fn(function() 66 | if not bufnr or not vim.api.nvim_buf_is_loaded(bufnr) then return end 67 | vim.lsp.util.buf_clear_references(bufnr) 68 | if cursor_in_references(bufnr) then 69 | local client = vim.lsp.get_client_by_id(client_id) 70 | if client then 71 | vim.lsp.util.buf_highlight_references(bufnr, result, client.offset_encoding) 72 | end 73 | end 74 | end, vim.g.Illuminate_delay or 17) 75 | table.sort(result, function(a, b) 76 | return before_by_start(a.range, b.range) 77 | end) 78 | references[bufnr] = result 79 | end 80 | 81 | local function valid(bufnr, range) 82 | return range 83 | and range.start.line < vim.api.nvim_buf_line_count(bufnr) 84 | and range.start.character < #vim.fn.getline(range.start.line + 1) 85 | end 86 | 87 | local function augroup(bufnr, autocmds) 88 | vim.cmd('augroup vim_illuminate_lsp' .. bufnr) 89 | vim.cmd('autocmd!') 90 | if autocmds then 91 | vim.b.illuminate_lsp_enabled = true 92 | autocmds() 93 | else 94 | vim.b.illuminate_lsp_enabled = false 95 | end 96 | vim.cmd('augroup END') 97 | end 98 | 99 | local function autocmd(bufnr) 100 | vim.cmd(string.format('autocmd CursorMoved,CursorMovedI lua require"illuminate".on_cursor_moved(%d)', 101 | bufnr, bufnr)) 102 | end 103 | 104 | local function move_cursor(row, col) 105 | if not paused_bufs[vim.api.nvim_get_current_buf()] then 106 | augroup(vim.api.nvim_get_current_buf(), function() 107 | vim.api.nvim_win_set_cursor(0, { row, col }) 108 | autocmd(vim.api.nvim_get_current_buf()) 109 | end) 110 | else 111 | vim.api.nvim_win_set_cursor(0, { row, col }) 112 | end 113 | end 114 | 115 | function M.on_attach(client) 116 | M.stop_buf() 117 | if client and not client.supports_method('textDocument/documentHighlight') then 118 | return 119 | end 120 | pcall(vim.api.nvim_command, 'IlluminationDisable!') 121 | augroup(vim.api.nvim_get_current_buf(), function() 122 | autocmd(vim.api.nvim_get_current_buf()) 123 | end) 124 | vim.lsp.handlers['textDocument/documentHighlight'] = function(...) 125 | if vim.fn.has('nvim-0.5.1') == 1 then 126 | handle_document_highlight(select(2, ...), select(3, ...).bufnr, select(3, ...).client_id) 127 | else 128 | handle_document_highlight(select(3, ...), select(5, ...), nil) 129 | end 130 | end 131 | vim.lsp.buf.document_highlight() 132 | end 133 | 134 | function M.on_cursor_moved(bufnr) 135 | if not cursor_in_references(bufnr) then 136 | vim.lsp.util.buf_clear_references(bufnr) 137 | end 138 | 139 | -- Best-effort check if any client support textDocument/documentHighlight 140 | local supported = nil 141 | -- For nvim 0.10+ 142 | if vim.lsp.get_clients then 143 | supported = false 144 | for _, client in ipairs(vim.lsp.get_clients({bufnr = bufnr})) do 145 | if client and client.supports_method('textDocument/documentHighlight') then 146 | supported = true 147 | end 148 | end 149 | -- For older versions 150 | elseif vim.lsp.for_each_buffer_client then 151 | supported = false 152 | vim.lsp.for_each_buffer_client(bufnr, function(client) 153 | if client.supports_method('textDocument/documentHighlight') then 154 | supported = true 155 | end 156 | end) 157 | end 158 | 159 | if supported == nil or supported then 160 | vim.lsp.buf.document_highlight() 161 | else 162 | augroup(vim.api.nvim_get_current_buf(), function() 163 | end) 164 | end 165 | end 166 | 167 | function M.get_document_highlights(bufnr) 168 | return references[bufnr] 169 | end 170 | 171 | function M.next_reference(opt) 172 | opt = vim.tbl_extend('force', { reverse = false, wrap = false, range_ordering = 'start', silent = false }, opt or {}) 173 | 174 | local before 175 | if opt.range_ordering == 'start' then 176 | before = before_by_start 177 | else 178 | before = before_disjoint 179 | end 180 | local bufnr = vim.api.nvim_get_current_buf() 181 | local refs = M.get_document_highlights(bufnr) 182 | if not refs or #refs == 0 then return nil end 183 | 184 | local next = nil 185 | local nexti = nil 186 | local crow, ccol = unpack(vim.api.nvim_win_get_cursor(0)) 187 | local crange = { start = { line = crow - 1, character = ccol } } 188 | 189 | for i, ref in ipairs(refs) do 190 | local range = ref.range 191 | if valid(bufnr, range) then 192 | if opt.reverse then 193 | if before(range, crange) and (not next or before(next, range)) then 194 | next = range 195 | nexti = i 196 | end 197 | else 198 | if before(crange, range) and (not next or before(range, next)) then 199 | next = range 200 | nexti = i 201 | end 202 | end 203 | end 204 | end 205 | if not next and opt.wrap then 206 | nexti = opt.reverse and #refs or 1 207 | next = refs[nexti].range 208 | end 209 | if next then 210 | move_cursor(next.start.line + 1, next.start.character) 211 | if not opt.silent then 212 | print('[' .. nexti .. '/' .. #refs .. ']') 213 | end 214 | end 215 | return next 216 | end 217 | 218 | function M.toggle_pause() 219 | if paused_bufs[vim.api.nvim_get_current_buf()] then 220 | paused_bufs[vim.api.nvim_get_current_buf()] = false 221 | augroup(vim.api.nvim_get_current_buf(), function() 222 | autocmd(vim.api.nvim_get_current_buf()) 223 | end) 224 | M.on_cursor_moved(vim.api.nvim_get_current_buf()) 225 | else 226 | paused_bufs[vim.api.nvim_get_current_buf()] = true 227 | augroup(vim.api.nvim_get_current_buf(), nil) 228 | end 229 | end 230 | 231 | function M.configure(config) 232 | require('illuminate.config').set(config) 233 | end 234 | 235 | function M.pause() 236 | require('illuminate.engine').pause() 237 | end 238 | 239 | function M.resume() 240 | require('illuminate.engine').resume() 241 | end 242 | 243 | function M.toggle() 244 | require('illuminate.engine').toggle() 245 | end 246 | 247 | function M.toggle_buf() 248 | require('illuminate.engine').toggle_buf() 249 | end 250 | 251 | function M.pause_buf() 252 | require('illuminate.engine').pause_buf() 253 | end 254 | 255 | function M.stop_buf() 256 | require('illuminate.engine').stop_buf() 257 | end 258 | 259 | function M.resume_buf() 260 | require('illuminate.engine').resume_buf() 261 | end 262 | 263 | function M.freeze_buf() 264 | require('illuminate.engine').freeze_buf() 265 | end 266 | 267 | function M.unfreeze_buf() 268 | require('illuminate.engine').unfreeze_buf() 269 | end 270 | 271 | function M.toggle_freeze_buf() 272 | require('illuminate.engine').toggle_freeze_buf() 273 | end 274 | 275 | function M.invisible_buf() 276 | require('illuminate.engine').invisible_buf() 277 | end 278 | 279 | function M.visible_buf() 280 | require('illuminate.engine').visible_buf() 281 | end 282 | 283 | function M.toggle_visibility_buf() 284 | require('illuminate.engine').toggle_visibility_buf() 285 | end 286 | 287 | function M.goto_next_reference(wrap) 288 | if wrap == nil then 289 | wrap = vim.o.wrapscan 290 | end 291 | require('illuminate.goto').goto_next_reference(wrap) 292 | end 293 | 294 | function M.goto_prev_reference(wrap) 295 | if wrap == nil then 296 | wrap = vim.o.wrapscan 297 | end 298 | require('illuminate.goto').goto_prev_reference(wrap) 299 | end 300 | 301 | function M.textobj_select() 302 | require('illuminate.textobj').select() 303 | end 304 | 305 | function M.debug() 306 | require('illuminate.engine').debug() 307 | end 308 | 309 | function M.is_paused() 310 | return require('illuminate.engine').is_paused() 311 | end 312 | 313 | function M.set_highlight_defaults() 314 | vim.cmd [[ 315 | hi def IlluminatedWordText guifg=none guibg=none gui=underline 316 | hi def IlluminatedWordRead guifg=none guibg=none gui=underline 317 | hi def IlluminatedWordWrite guifg=none guibg=none gui=underline 318 | ]] 319 | end 320 | 321 | return M 322 | -------------------------------------------------------------------------------- /lua/illuminate/config.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local config = { 4 | providers = { 5 | 'lsp', 6 | 'treesitter', 7 | 'regex', 8 | }, 9 | delay = 100, 10 | filetype_overrides = {}, 11 | filetypes_denylist = { 12 | 'dirbuf', 13 | 'dirvish', 14 | 'fugitive', 15 | }, 16 | filetypes_allowlist = {}, 17 | modes_denylist = {}, 18 | modes_allowlist = {}, 19 | providers_regex_syntax_denylist = {}, 20 | providers_regex_syntax_allowlist = {}, 21 | under_cursor = true, 22 | max_file_lines = nil, 23 | large_file_cutoff = 10000, 24 | large_file_config = nil, 25 | min_count_to_highlight = 1, 26 | should_enable = nil, 27 | case_insensitive_regex = false, 28 | disable_keymaps = false, 29 | } 30 | 31 | function M.set(config_overrides) 32 | config = vim.tbl_extend('force', config, config_overrides or {}) 33 | end 34 | 35 | function M.get_raw() 36 | return config 37 | end 38 | 39 | function M.get() 40 | return ( 41 | M.large_file_cutoff() == nil 42 | or vim.fn.line('$') <= M.large_file_cutoff() 43 | or M.large_file_overrides() == nil 44 | ) 45 | and config 46 | or M.large_file_overrides() 47 | end 48 | 49 | function M.filetype_override(bufnr) 50 | local ft = vim.api.nvim_buf_get_option(bufnr, 'filetype') 51 | return M.get()['filetype_overrides'] and M.get()['filetype_overrides'][ft] or {} 52 | end 53 | 54 | function M.providers(bufnr) 55 | return M.filetype_override(bufnr)['providers'] or M.get()['providers'] 56 | end 57 | 58 | function M.filetypes_denylist() 59 | return M.get()['filetypes_denylist'] or {} 60 | end 61 | 62 | function M.filetypes_allowlist() 63 | return M.get()['filetypes_allowlist'] or {} 64 | end 65 | 66 | function M.modes_denylist(bufnr) 67 | return M.filetype_override(bufnr)['modes_denylist'] or M.get()['modes_denylist'] or {} 68 | end 69 | 70 | function M.modes_allowlist(bufnr) 71 | return M.filetype_override(bufnr)['modes_allowlist'] or M.get()['modes_allowlist'] or {} 72 | end 73 | 74 | function M.provider_regex_syntax_denylist(bufnr) 75 | return M.filetype_override(bufnr)['providers_regex_syntax_denylist'] 76 | or M.get()['providers_regex_syntax_denylist'] 77 | or {} 78 | end 79 | 80 | function M.provider_regex_syntax_allowlist(bufnr) 81 | return M.filetype_override(bufnr)['providers_regex_syntax_allowlist'] 82 | or M.get()['providers_regex_syntax_allowlist'] 83 | or {} 84 | end 85 | 86 | function M.under_cursor(bufnr) 87 | if M.filetype_override(bufnr)['under_cursor'] ~= nil then 88 | return M.filetype_override(bufnr)['under_cursor'] ~= nil 89 | end 90 | return M.get()['under_cursor'] 91 | end 92 | 93 | function M.delay(bufnr) 94 | local delay = M.filetype_override(bufnr)['delay'] or M.get()['delay'] or 17 95 | if string.sub(vim.api.nvim_get_mode().mode, 1, 1) == 'i' then 96 | delay = delay + 100 97 | end 98 | if delay < 17 then 99 | return 17 100 | end 101 | return delay 102 | end 103 | 104 | function M.max_file_lines() 105 | return M.get()['max_file_lines'] 106 | end 107 | 108 | function M.large_file_cutoff() 109 | return config['large_file_cutoff'] 110 | end 111 | 112 | function M.large_file_overrides() 113 | if config['large_file_overrides'] ~= nil then 114 | if config['large_file_overrides']['under_cursor'] == nil then 115 | config['large_file_overrides']['under_cursor'] = true 116 | end 117 | return config['large_file_overrides'] 118 | end 119 | return { 120 | filetypes_allowlist = { '_none' } 121 | } 122 | end 123 | 124 | function M.min_count_to_highlight() 125 | return M.get()['min_count_to_highlight'] or 1 126 | end 127 | 128 | function M.should_enable() 129 | return M.get()['should_enable'] or function(_) 130 | return true 131 | end 132 | end 133 | 134 | function M.case_insensitive_regex() 135 | return M.get()['case_insensitive_regex'] 136 | end 137 | 138 | function M.disable_keymaps() 139 | return M.get()['disable_keymaps'] 140 | end 141 | 142 | return M 143 | -------------------------------------------------------------------------------- /lua/illuminate/engine.lua: -------------------------------------------------------------------------------- 1 | local hl = require('illuminate.highlight') 2 | local ref = require('illuminate.reference') 3 | local config = require('illuminate.config') 4 | local util = require('illuminate.util') 5 | 6 | local M = {} 7 | 8 | local AUGROUP = 'vim_illuminate_v2_augroup' 9 | local timers = {} 10 | local paused_bufs = {} 11 | local stopped_bufs = {} 12 | local is_paused = false 13 | local written = {} 14 | local error_timestamps = {} 15 | local frozen_bufs = {} 16 | local invisible_bufs = {} 17 | local started = false 18 | 19 | local function buf_should_illuminate(bufnr) 20 | if is_paused or paused_bufs[bufnr] or stopped_bufs[bufnr] then 21 | return false 22 | end 23 | 24 | return config.should_enable()(bufnr) 25 | and (config.max_file_lines() == nil or vim.fn.line('$') <= config.max_file_lines()) 26 | and util.is_allowed( 27 | config.modes_allowlist(bufnr), 28 | config.modes_denylist(bufnr), 29 | vim.api.nvim_get_mode().mode 30 | ) and util.is_allowed( 31 | config.filetypes_allowlist(), 32 | config.filetypes_denylist(), 33 | vim.api.nvim_buf_get_option(bufnr, 'filetype') 34 | ) 35 | end 36 | 37 | local function stop_timer(timer) 38 | if vim.loop.is_active(timer) then 39 | vim.loop.timer_stop(timer) 40 | vim.loop.close(timer) 41 | end 42 | end 43 | 44 | function M.start() 45 | started = true 46 | vim.api.nvim_create_augroup(AUGROUP, { clear = true }) 47 | vim.api.nvim_create_autocmd({ 'VimEnter', 'CursorMoved', 'CursorMovedI', 'ModeChanged', 'TextChanged' }, { 48 | group = AUGROUP, 49 | callback = function() 50 | M.refresh_references() 51 | end, 52 | }) 53 | 54 | -- Set up auto-attach/detach for the treesitter provider if we can use builtin methods instead of 55 | -- treesitter modules. 56 | if vim.fn.has('nvim-0.9') == 1 then 57 | vim.api.nvim_create_autocmd({ 'FileType' }, { 58 | callback = function(details) 59 | require('illuminate.providers.treesitter').detach(details.buf) 60 | 61 | local lang = vim.treesitter.language.get_lang(details.match) 62 | local ok, query = pcall(require, 'nvim-treesitter.query') 63 | if not ok then 64 | return 65 | end 66 | 67 | local parsers 68 | ok, parsers = pcall(require, 'nvim-treesitter.parsers') 69 | if not ok then 70 | return 71 | end 72 | 73 | if not parsers.has_parser(lang) then 74 | return false 75 | end 76 | 77 | if not lang or not query.has_locals(lang) then 78 | return 79 | end 80 | 81 | require('illuminate.providers.treesitter').attach(details.buf) 82 | end, 83 | }) 84 | vim.api.nvim_create_autocmd({ 'BufUnload' }, { 85 | callback = function(details) 86 | require('illuminate.providers.treesitter').detach(details.buf) 87 | end, 88 | }) 89 | end 90 | 91 | -- If vim.lsp.buf.format is called, this will call vim.api.nvim_buf_set_text which messes up extmarks. 92 | -- By using this `written` variable, we can ensure refresh_references doesn't terminate early based on 93 | -- ref.buf_cursor_in_references being incorrect (we have references but they're not actually showing 94 | -- as illuminated). vim.lsp.buf.format will trigger CursorMoved so we don't need to do it here. 95 | vim.api.nvim_create_autocmd({ 'BufWritePost' }, { 96 | group = AUGROUP, 97 | callback = function() 98 | written[vim.api.nvim_get_current_buf()] = true 99 | end, 100 | }) 101 | vim.api.nvim_create_autocmd({ 'VimLeave' }, { 102 | group = AUGROUP, 103 | callback = function() 104 | for _, timer in pairs(timers) do 105 | stop_timer(timer) 106 | end 107 | end, 108 | }) 109 | end 110 | 111 | function M.stop() 112 | started = false 113 | vim.api.nvim_create_augroup(AUGROUP, { clear = true }) 114 | end 115 | 116 | --- Get the highlighted references for the item under the cursor for 117 | --- @bufnr and clears any old reference highlights 118 | --- 119 | --- @bufnr (number) 120 | function M.refresh_references(bufnr, winid) 121 | bufnr = bufnr or vim.api.nvim_get_current_buf() 122 | winid = winid or vim.api.nvim_get_current_win() 123 | 124 | if frozen_bufs[bufnr] then 125 | return 126 | end 127 | 128 | if not buf_should_illuminate(bufnr) then 129 | hl.buf_clear_references(bufnr) 130 | ref.buf_set_references(bufnr, {}) 131 | return 132 | end 133 | 134 | -- We might want to optimize here by returning early if cursor is in references. 135 | -- The downside is that LSP servers can sometimes return a different list of references 136 | -- as you move around an existing reference (like return statements). 137 | if written[bufnr] or not ref.buf_cursor_in_references(bufnr, util.get_cursor_pos(winid)) then 138 | hl.buf_clear_references(bufnr) 139 | ref.buf_set_references(bufnr, {}) 140 | elseif config.large_file_cutoff() ~= nil and vim.fn.line('$') > config.large_file_cutoff() then 141 | return 142 | end 143 | written[bufnr] = nil 144 | 145 | if timers[bufnr] then 146 | stop_timer(timers[bufnr]) 147 | end 148 | 149 | local provider = M.get_provider(bufnr) 150 | if not provider then return end 151 | pcall(provider['initiate_request'], bufnr, winid) 152 | 153 | local changedtick = vim.api.nvim_buf_get_changedtick(bufnr) 154 | 155 | local timer = vim.loop.new_timer() 156 | timers[bufnr] = timer 157 | timer:start(config.delay(bufnr), 17, vim.schedule_wrap(function() 158 | local ok, err = pcall(function() 159 | if not bufnr or not vim.api.nvim_buf_is_loaded(bufnr) then 160 | stop_timer(timer) 161 | return 162 | end 163 | 164 | hl.buf_clear_references(bufnr) 165 | ref.buf_set_references(bufnr, {}) 166 | 167 | if not buf_should_illuminate(bufnr) then 168 | stop_timer(timer) 169 | return 170 | end 171 | 172 | if vim.api.nvim_buf_get_changedtick(bufnr) ~= changedtick 173 | or vim.api.nvim_get_current_win() ~= winid 174 | or bufnr ~= vim.api.nvim_win_get_buf(0) then 175 | stop_timer(timer) 176 | return 177 | end 178 | 179 | provider = M.get_provider(bufnr) 180 | if not provider then 181 | stop_timer(timer) 182 | return 183 | end 184 | 185 | local references = provider.get_references(bufnr, util.get_cursor_pos(winid)) 186 | if references ~= nil then 187 | ref.buf_set_references(bufnr, references) 188 | if ref.buf_cursor_in_references(bufnr, util.get_cursor_pos(winid)) then 189 | if not invisible_bufs[bufnr] == true then 190 | hl.buf_highlight_references(bufnr, ref.buf_get_references(bufnr)) 191 | end 192 | else 193 | ref.buf_set_references(bufnr, {}) 194 | end 195 | stop_timer(timer) 196 | end 197 | end) 198 | 199 | if not ok then 200 | local time = vim.loop.hrtime() 201 | if #error_timestamps == 5 then 202 | vim.notify( 203 | 'vim-illuminate: An internal error has occured: ' .. vim.inspect(ok) .. vim.inspect(err), 204 | vim.log.levels.ERROR, 205 | {} 206 | ) 207 | M.stop() 208 | stop_timer(timer) 209 | elseif #error_timestamps == 0 or time - error_timestamps[#error_timestamps] < 500000000 then 210 | table.insert(error_timestamps, time) 211 | else 212 | error_timestamps = { time } 213 | end 214 | end 215 | end)) 216 | end 217 | 218 | function M.get_provider(bufnr) 219 | for _, provider in ipairs(config.providers(bufnr) or {}) do 220 | local ok, providerModule = pcall(require, string.format('illuminate.providers.%s', provider)) 221 | if ok and providerModule.is_ready(bufnr) then 222 | return providerModule, provider 223 | end 224 | end 225 | return nil 226 | end 227 | 228 | function M.pause() 229 | is_paused = true 230 | M.refresh_references() 231 | end 232 | 233 | function M.resume() 234 | is_paused = false 235 | M.refresh_references() 236 | end 237 | 238 | function M.toggle() 239 | is_paused = not is_paused 240 | M.refresh_references() 241 | end 242 | 243 | function M.toggle_buf(bufnr) 244 | bufnr = bufnr or vim.api.nvim_get_current_buf() 245 | if paused_bufs[bufnr] then 246 | paused_bufs[bufnr] = nil 247 | else 248 | paused_bufs[bufnr] = true 249 | end 250 | M.refresh_references() 251 | end 252 | 253 | function M.pause_buf(bufnr) 254 | paused_bufs[bufnr or vim.api.nvim_get_current_buf()] = true 255 | M.refresh_references() 256 | end 257 | 258 | function M.resume_buf(bufnr) 259 | paused_bufs[bufnr or vim.api.nvim_get_current_buf()] = nil 260 | M.refresh_references() 261 | end 262 | 263 | function M.stop_buf(bufnr) 264 | stopped_bufs[bufnr or vim.api.nvim_get_current_buf()] = true 265 | M.refresh_references() 266 | end 267 | 268 | function M.freeze_buf(bufnr) 269 | frozen_bufs[bufnr or vim.api.nvim_get_current_buf()] = true 270 | end 271 | 272 | function M.unfreeze_buf(bufnr) 273 | frozen_bufs[bufnr or vim.api.nvim_get_current_buf()] = nil 274 | end 275 | 276 | function M.toggle_freeze_buf(bufnr) 277 | bufnr = bufnr or vim.api.nvim_get_current_buf() 278 | frozen_bufs[bufnr] = not frozen_bufs[bufnr] 279 | end 280 | 281 | function M.invisible_buf(bufnr) 282 | invisible_bufs[bufnr or vim.api.nvim_get_current_buf()] = true 283 | M.refresh_references() 284 | end 285 | 286 | function M.visible_buf(bufnr) 287 | invisible_bufs[bufnr or vim.api.nvim_get_current_buf()] = nil 288 | M.refresh_references() 289 | end 290 | 291 | function M.toggle_visibility_buf(bufnr) 292 | bufnr = bufnr or vim.api.nvim_get_current_buf() 293 | invisible_bufs[bufnr] = not invisible_bufs[bufnr] 294 | M.refresh_references() 295 | end 296 | 297 | function M.debug() 298 | local bufnr = vim.api.nvim_get_current_buf() 299 | print('buf_should_illuminate', bufnr, buf_should_illuminate(bufnr)) 300 | print('config', vim.inspect(config.get_raw())) 301 | print('started', started) 302 | print('provider', M.get_provider(bufnr)) 303 | print('`termguicolors`', vim.opt.termguicolors:get()) 304 | end 305 | 306 | function M.is_paused() 307 | return is_paused 308 | end 309 | 310 | return M 311 | -------------------------------------------------------------------------------- /lua/illuminate/goto.lua: -------------------------------------------------------------------------------- 1 | local util = require('illuminate.util') 2 | local ref = require('illuminate.reference') 3 | local engine = require('illuminate.engine') 4 | 5 | local M = {} 6 | 7 | function M.goto_next_reference(wrap) 8 | local bufnr = vim.api.nvim_get_current_buf() 9 | local winid = vim.api.nvim_get_current_win() 10 | local cursor_pos = util.get_cursor_pos(winid) 11 | 12 | if #ref.buf_get_references(bufnr) == 0 then 13 | return 14 | end 15 | 16 | local i = ref.bisect_left(ref.buf_get_references(bufnr), cursor_pos) 17 | i = i + 1 18 | if i > #ref.buf_get_references(bufnr) then 19 | if wrap then 20 | i = 1 21 | else 22 | vim.api.nvim_err_writeln("E384: vim-illuminate: goto_next_reference hit BOTTOM of the references") 23 | return 24 | end 25 | end 26 | 27 | local pos, _ = unpack(ref.buf_get_references(bufnr)[i]) 28 | local new_cursor_pos = { pos[1] + 1, pos[2] } 29 | vim.cmd('normal! m`') 30 | engine.freeze_buf(bufnr) 31 | vim.api.nvim_win_set_cursor(winid, new_cursor_pos) 32 | engine.unfreeze_buf(bufnr) 33 | end 34 | 35 | function M.goto_prev_reference(wrap) 36 | local bufnr = vim.api.nvim_get_current_buf() 37 | local winid = vim.api.nvim_get_current_win() 38 | local cursor_pos = util.get_cursor_pos(winid) 39 | 40 | if #ref.buf_get_references(bufnr) == 0 then 41 | return 42 | end 43 | 44 | local i = ref.bisect_left(ref.buf_get_references(bufnr), cursor_pos) 45 | i = i - 1 46 | if i == 0 then 47 | if wrap then 48 | i = #ref.buf_get_references(bufnr) 49 | else 50 | vim.api.nvim_err_writeln("E384: vim-illuminate: goto_prev_reference hit TOP of the references") 51 | return 52 | end 53 | end 54 | 55 | local pos, _ = unpack(ref.buf_get_references(bufnr)[i]) 56 | local new_cursor_pos = { pos[1] + 1, pos[2] } 57 | vim.cmd('normal! m`') 58 | engine.freeze_buf(bufnr) 59 | vim.api.nvim_win_set_cursor(winid, new_cursor_pos) 60 | engine.unfreeze_buf(bufnr) 61 | end 62 | 63 | return M 64 | -------------------------------------------------------------------------------- /lua/illuminate/highlight.lua: -------------------------------------------------------------------------------- 1 | local util = require('illuminate.util') 2 | local config = require('illuminate.config') 3 | local ref = require('illuminate.reference') 4 | 5 | local M = {} 6 | 7 | local HL_NAMESPACE = vim.api.nvim_create_namespace('illuminate.highlight') 8 | 9 | local function kind_to_hl_group(kind) 10 | return kind == vim.lsp.protocol.DocumentHighlightKind.Text and 'IlluminatedWordText' 11 | or kind == vim.lsp.protocol.DocumentHighlightKind.Read and 'IlluminatedWordRead' 12 | or kind == vim.lsp.protocol.DocumentHighlightKind.Write and 'IlluminatedWordWrite' 13 | or 'IlluminatedWordText' 14 | end 15 | 16 | function M.buf_highlight_references(bufnr, references) 17 | if config.min_count_to_highlight() > #references then 18 | return 19 | end 20 | 21 | local cursor_pos = util.get_cursor_pos() 22 | for _, reference in ipairs(references) do 23 | if config.under_cursor(bufnr) or not ref.is_pos_in_ref(cursor_pos, reference) then 24 | M.range( 25 | bufnr, 26 | reference[1], 27 | reference[2], 28 | reference[3] 29 | ) 30 | end 31 | end 32 | end 33 | 34 | function M.range(bufnr, start, finish, kind) 35 | if vim.fn.has('nvim-0.11') == 1 then 36 | local start_l, start_col = unpack(start) 37 | local finish_l, finish_col = unpack(finish) 38 | start = { bufnr, start_l + 1, start_col + 1 } 39 | finish = { bufnr, finish_l + 1, finish_col + 1 } 40 | 41 | local region = vim.fn.getregionpos(start, finish, { type = "v", exclusive = true }) 42 | 43 | for _, segment in ipairs(region) do 44 | local start_pos, finish_pos = unpack(segment) 45 | vim.api.nvim_buf_set_extmark(bufnr, HL_NAMESPACE, start_pos[2] - 1, start_pos[3] - 1, { 46 | hl_group = kind_to_hl_group(kind), 47 | end_col = finish_pos[3], 48 | priority = 199, 49 | strict = false, 50 | }) 51 | end 52 | else 53 | local region = vim.region(bufnr, start, finish, 'v', false) 54 | for linenr, cols in pairs(region) do 55 | if linenr == -1 then 56 | linenr = 0 57 | end 58 | local end_row 59 | if cols[2] == -1 then 60 | end_row = linenr + 1 61 | cols[2] = 0 62 | end 63 | vim.api.nvim_buf_set_extmark(bufnr, HL_NAMESPACE, linenr, cols[1], { 64 | hl_group = kind_to_hl_group(kind), 65 | end_row = end_row, 66 | end_col = cols[2], 67 | priority = 199, 68 | strict = false, 69 | }) 70 | end 71 | end 72 | end 73 | 74 | function M.buf_clear_references(bufnr) 75 | vim.api.nvim_buf_clear_namespace(bufnr, HL_NAMESPACE, 0, -1) 76 | end 77 | 78 | return M 79 | -------------------------------------------------------------------------------- /lua/illuminate/providers/lsp.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local bufs = {} 4 | 5 | local function _str_byteindex_enc(line, col, encoding) 6 | if not encoding then 7 | encoding = 'utf-16' 8 | end 9 | 10 | if vim.fn.has('nvim-0.11') == 1 then 11 | return vim.str_byteindx(line, encoding, col, false) 12 | end 13 | 14 | if encoding == 'utf-8' then 15 | if col then 16 | return col 17 | else 18 | return #line 19 | end 20 | elseif encoding == 'utf-16' then 21 | return vim.str_byteindex(line, col, true) 22 | elseif encoding == 'utf-32' then 23 | return vim.str_byteindex(line, col) 24 | else 25 | return col 26 | end 27 | end 28 | 29 | local function get_line_byte_from_position(bufnr, line, col, offset_encoding) 30 | if col == 0 then 31 | return col 32 | end 33 | 34 | local lines = vim.api.nvim_buf_get_lines(bufnr, line, line + 1, false) 35 | if not lines or #lines == 0 then 36 | return col 37 | end 38 | local ok, result = pcall(_str_byteindex_enc, lines[1], col, offset_encoding) 39 | if ok then 40 | return result 41 | end 42 | return math.min(#(lines[1]), col) 43 | end 44 | 45 | function M.get_references(bufnr) 46 | if not bufs[bufnr] or not bufs[bufnr][3] then 47 | return nil 48 | end 49 | 50 | return bufs[bufnr][3] 51 | end 52 | 53 | function M.is_ready(bufnr) 54 | local supported = false 55 | -- For nvim 0.10+ 56 | if vim.lsp.get_clients then 57 | supported = false 58 | for _, client in ipairs(vim.lsp.get_clients({ bufnr = bufnr })) do 59 | if client then 60 | supported = vim.fn.has('nvim-0.11') == 1 and client:supports_method('textDocument/documentHighlight') 61 | or vim.fn.has('nvim-0.11') == 0 and client.supports_method('textDocument/documentHighlight') 62 | if supported then 63 | break 64 | end 65 | end 66 | end 67 | -- For older versions 68 | elseif vim.lsp.for_each_buffer_client then 69 | supported = false 70 | vim.lsp.for_each_buffer_client(bufnr, function(client) 71 | if client and client.supports_method('textDocument/documentHighlight') then 72 | supported = true 73 | end 74 | end) 75 | end 76 | return supported 77 | end 78 | 79 | function M.initiate_request(bufnr, winid) 80 | local id = 1 81 | if bufs[bufnr] then 82 | local prev_id, cancel_fn, references = unpack(bufs[bufnr]) 83 | if references == nil then 84 | pcall(cancel_fn) 85 | end 86 | id = prev_id + 1 87 | end 88 | 89 | local params = vim.fn.has('nvim-0.11') == 1 and function(client) 90 | return vim.lsp.util.make_position_params(winid, client.offset_encoding) 91 | end or vim.lsp.util.make_position_params(winid) 92 | 93 | local cancel_fn = vim.lsp.buf_request_all( 94 | bufnr, 95 | 'textDocument/documentHighlight', 96 | params, 97 | function(client_results) 98 | if bufs[bufnr][1] ~= id then 99 | return 100 | end 101 | if not vim.api.nvim_buf_is_valid(bufnr) then 102 | bufs[bufnr][3] = {} 103 | return 104 | end 105 | 106 | local references = {} 107 | for client_id, results in pairs(client_results) do 108 | local client = vim.lsp.get_client_by_id(client_id) 109 | if client and results['result'] then 110 | for _, res in ipairs(results['result']) do 111 | local start_col = get_line_byte_from_position( 112 | bufnr, 113 | res['range']['start']['line'], 114 | res['range']['start']['character'], 115 | res['offset_encoding'] 116 | ) 117 | local end_col = get_line_byte_from_position( 118 | bufnr, 119 | res['range']['end']['line'], 120 | res['range']['end']['character'], 121 | res['offset_encoding'] 122 | ) 123 | table.insert(references, { 124 | { res['range']['start']['line'], start_col }, 125 | { res['range']['end']['line'], end_col }, 126 | res['kind'], 127 | }) 128 | end 129 | end 130 | end 131 | 132 | bufs[bufnr][3] = references 133 | end 134 | ) 135 | 136 | bufs[bufnr] = { 137 | id, 138 | cancel_fn, 139 | } 140 | end 141 | 142 | return M 143 | -------------------------------------------------------------------------------- /lua/illuminate/providers/regex.lua: -------------------------------------------------------------------------------- 1 | local config = require('illuminate.config') 2 | local util = require('illuminate.util') 3 | 4 | local M = {} 5 | 6 | local START_WORD_REGEX = vim.regex([[^\k*]]) 7 | local END_WORD_REGEX = vim.regex([[\k*$]]) 8 | 9 | -- foo 10 | -- foo 11 | -- Foo 12 | -- fOo 13 | local function get_cur_word(bufnr, cursor) 14 | local line = vim.api.nvim_buf_get_lines(bufnr, cursor[1], cursor[1] + 1, false)[1] 15 | local left_part = string.sub(line, 0, cursor[2] + 1) 16 | local right_part = string.sub(line, cursor[2] + 1) 17 | local start_idx, _ = END_WORD_REGEX:match_str(left_part) 18 | local _, end_idx = START_WORD_REGEX:match_str(right_part) 19 | local word = string.format('%s%s', string.sub(left_part, start_idx + 1), string.sub(right_part, 2, end_idx)) 20 | local modifiers = [[\V]] 21 | if config.case_insensitive_regex() then 22 | modifiers = modifiers .. [[\c]] 23 | end 24 | local ok, escaped = pcall(vim.fn.escape, word, [[/\]]) 25 | if ok then 26 | return modifiers .. [[\<]] .. escaped .. [[\>]] 27 | end 28 | end 29 | 30 | function M.get_references(bufnr, cursor) 31 | local refs = {} 32 | local ok, re = pcall(vim.regex, get_cur_word(bufnr, cursor)) 33 | if not ok then 34 | return refs 35 | end 36 | 37 | local line_count = vim.api.nvim_buf_line_count(bufnr) 38 | for i = 0, line_count - 1 do 39 | local start_byte, end_byte = 0, 0 40 | while true do 41 | local start_offset, end_offset = re:match_line(bufnr, i, end_byte) 42 | if not start_offset then break end 43 | 44 | start_byte = end_byte + start_offset 45 | end_byte = end_byte + end_offset 46 | table.insert(refs, { 47 | { i, start_byte }, 48 | { i, end_byte }, 49 | vim.lsp.protocol.DocumentHighlightKind.Text, 50 | }) 51 | end 52 | end 53 | 54 | return refs 55 | end 56 | 57 | function M.is_ready(bufnr) 58 | local name = vim.fn.synIDattr( 59 | vim.fn.synIDtrans( 60 | vim.fn.synID(vim.fn.line('.'), vim.fn.col('.'), 1) 61 | ), 62 | 'name' 63 | ) 64 | if util.is_allowed( 65 | config.provider_regex_syntax_allowlist(bufnr), 66 | config.provider_regex_syntax_denylist(bufnr), 67 | name 68 | ) then 69 | return true 70 | end 71 | return false 72 | end 73 | 74 | return M 75 | -------------------------------------------------------------------------------- /lua/illuminate/providers/treesitter.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local buf_attached = {} 4 | 5 | -- get_node is builtin in v0.9+, get_node_at_cursor is for older versions 6 | local get_node_at_cursor = vim.treesitter.get_node or require('nvim-treesitter.ts_utils').get_node_at_cursor 7 | function M.get_references(bufnr) 8 | local ok, locals = pcall(require, 'nvim-treesitter.locals') 9 | if not ok then 10 | return 11 | end 12 | 13 | local node_at_point = get_node_at_cursor() 14 | if not node_at_point then 15 | return 16 | end 17 | 18 | local refs = {} 19 | local def_node, scope, kind = locals.find_definition(node_at_point, bufnr) 20 | local usages = locals.find_usages(def_node, scope, bufnr) 21 | for _, node in ipairs(usages) do 22 | if kind ~= nil and node == def_node then 23 | local range = { def_node:range() } 24 | table.insert(refs, { 25 | { range[1], range[2] }, 26 | { range[3], range[4] }, 27 | vim.lsp.protocol.DocumentHighlightKind.Write, 28 | }) 29 | else 30 | local range = { node:range() } 31 | table.insert(refs, { 32 | { range[1], range[2] }, 33 | { range[3], range[4] }, 34 | vim.lsp.protocol.DocumentHighlightKind.Read, 35 | }) 36 | end 37 | end 38 | 39 | return refs 40 | end 41 | 42 | function M.is_ready(bufnr) 43 | return buf_attached[bufnr] and vim.api.nvim_buf_get_option(bufnr, 'filetype') ~= 'yaml' 44 | end 45 | 46 | function M.attach(bufnr) 47 | buf_attached[bufnr] = true 48 | end 49 | 50 | function M.detach(bufnr) 51 | buf_attached[bufnr] = nil 52 | end 53 | 54 | return M 55 | -------------------------------------------------------------------------------- /lua/illuminate/reference.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | -- @table pos 4 | -- @field 1 (number) line (0-indexed) 5 | -- @field 2 (number) col (0-indexed) 6 | -- 7 | -- @table ref 8 | -- @field 1 (table) start pos 9 | -- @field 2 (table) end pos 10 | 11 | local buf_references = {} 12 | 13 | local function get_references(bufnr) 14 | return buf_references[bufnr] or {} 15 | end 16 | 17 | local function pos_before(pos1, pos2) 18 | if pos1[1] < pos2[1] then return true end 19 | if pos1[1] > pos2[1] then return false end 20 | if pos1[2] < pos2[2] then return true end 21 | return false 22 | end 23 | 24 | local function pos_equal(pos1, pos2) 25 | return pos1[1] == pos2[1] and pos1[2] == pos2[2] 26 | end 27 | 28 | local function ref_before(ref1, ref2) 29 | return pos_before(ref1[1], ref2[1]) or pos_equal(ref1[1], ref2[1]) and pos_before(ref1[2], ref2[2]) 30 | end 31 | 32 | local function buf_sort_references(bufnr) 33 | local should_sort = false 34 | for i, ref in ipairs(get_references(bufnr)) do 35 | if i > 1 then 36 | if not ref_before(get_references(bufnr)[i - 1], ref) then 37 | should_sort = true 38 | break 39 | end 40 | end 41 | end 42 | 43 | if should_sort then 44 | table.sort(get_references(bufnr), ref_before) 45 | end 46 | end 47 | 48 | function M.is_pos_in_ref(pos, ref) 49 | return (pos_before(ref[1], pos) or pos_equal(ref[1], pos)) and (pos_before(pos, ref[2]) or pos_equal(pos, ref[2])) 50 | end 51 | 52 | function M.bisect_left(references, pos) 53 | local l, r = 1, #references + 1 54 | while l < r do 55 | local m = l + math.floor((r - l) / 2) 56 | if pos_before(references[m][2], pos) then 57 | l = m + 1 58 | else 59 | r = m 60 | end 61 | end 62 | return l 63 | end 64 | 65 | function M.buf_get_references(bufnr) 66 | return get_references(bufnr) 67 | end 68 | 69 | function M.buf_set_references(bufnr, references) 70 | buf_references[bufnr] = references 71 | buf_sort_references(bufnr) 72 | end 73 | 74 | function M.buf_cursor_in_references(bufnr, cursor_pos) 75 | if not get_references(bufnr) then 76 | return false 77 | end 78 | 79 | local i = M.bisect_left(get_references(bufnr), cursor_pos) 80 | 81 | if i > #get_references(bufnr) then 82 | return false 83 | end 84 | if not M.is_pos_in_ref(cursor_pos, get_references(bufnr)[i]) then 85 | return false 86 | end 87 | 88 | return true 89 | end 90 | 91 | return M 92 | -------------------------------------------------------------------------------- /lua/illuminate/textobj.lua: -------------------------------------------------------------------------------- 1 | local util = require('illuminate.util') 2 | local ref = require('illuminate.reference') 3 | 4 | local M = {} 5 | 6 | local visual_modes = { 7 | ['v'] = true, 8 | ['vs'] = true, 9 | ['V'] = true, 10 | ['Vs'] = true, 11 | ['CTRL-V'] = true, 12 | ['CTRL-Vs'] = true, 13 | ['s'] = true, 14 | ['S'] = true, 15 | ['CTRL-S'] = true, 16 | } 17 | 18 | function M.select() 19 | local bufnr = vim.api.nvim_get_current_buf() 20 | local winid = vim.api.nvim_get_current_win() 21 | local cursor_pos = util.get_cursor_pos(winid) 22 | 23 | if #ref.buf_get_references(bufnr) == 0 then 24 | return 25 | end 26 | 27 | local i = ref.bisect_left(ref.buf_get_references(bufnr), cursor_pos) 28 | if i > #ref.buf_get_references(bufnr) then 29 | return 30 | end 31 | 32 | local reference = ref.buf_get_references(bufnr)[i] 33 | vim.api.nvim_win_set_cursor(winid, { reference[1][1] + 1, reference[1][2] }) 34 | if not visual_modes[vim.api.nvim_get_mode().mode] then 35 | vim.cmd('normal! v') 36 | else 37 | vim.cmd('normal! o') 38 | end 39 | vim.api.nvim_win_set_cursor(winid, { reference[2][1] + 1, reference[2][2] - 1 }) 40 | end 41 | 42 | return M 43 | -------------------------------------------------------------------------------- /lua/illuminate/util.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | function M.get_cursor_pos(winid) 4 | winid = winid or vim.api.nvim_get_current_win() 5 | local cursor = vim.api.nvim_win_get_cursor(winid) 6 | cursor[1] = cursor[1] - 1 -- we always want line to be 0-indexed 7 | return cursor 8 | end 9 | 10 | function M.list_to_set(list) 11 | if list == nil then 12 | return nil 13 | end 14 | 15 | local set = {} 16 | for _, v in pairs(list) do 17 | set[v] = true 18 | end 19 | return set 20 | end 21 | 22 | function M.is_allowed(allow_list, deny_list, thing) 23 | if #allow_list == 0 and #deny_list == 0 then 24 | return true 25 | end 26 | 27 | if #deny_list > 0 then 28 | return not vim.tbl_contains(deny_list, thing) 29 | end 30 | 31 | return vim.tbl_contains(allow_list, thing) 32 | end 33 | 34 | function M.tbl_get(tbl, expected_type, ...) 35 | local cur = tbl 36 | for _, key in ipairs({ ... }) do 37 | if type(cur) ~= 'table' or cur[key] == nil then 38 | return nil 39 | end 40 | 41 | cur = cur[key] 42 | end 43 | 44 | return type(cur) == expected_type and cur or nil 45 | end 46 | 47 | function M.has_keymap(mode, lhs) 48 | return vim.fn.mapcheck(lhs, mode) ~= '' 49 | end 50 | 51 | return M 52 | -------------------------------------------------------------------------------- /plugin/illuminate.vim: -------------------------------------------------------------------------------- 1 | " illuminate.vim - Vim plugin for selectively illuminating other uses of current word 2 | " Maintainer: Adam P. Regasz-Rethy (RRethy) 3 | " Version: 2.0 4 | 5 | if exists('g:loaded_illuminate') 6 | finish 7 | endif 8 | 9 | let g:loaded_illuminate = 1 10 | 11 | if has('nvim-0.7.2') && get(g:, 'Illuminate_useDeprecated', 0) != 1 12 | lua << EOF 13 | if vim.fn.has('nvim-0.9') == 0 then 14 | -- fallback to nvim-treesitter modules 15 | local ok, ts = pcall(require, 'nvim-treesitter') 16 | if ok then 17 | ts.define_modules({ 18 | illuminate = { 19 | module_path = 'illuminate.providers.treesitter', 20 | enable = true, 21 | disable = {}, 22 | is_supported = require('nvim-treesitter.query').has_locals, 23 | } 24 | }) 25 | end 26 | end 27 | require('illuminate.engine').start() 28 | vim.api.nvim_create_user_command('IlluminatePause', require('illuminate').pause, { bang = true }) 29 | vim.api.nvim_create_user_command('IlluminateResume', require('illuminate').resume, { bang = true }) 30 | vim.api.nvim_create_user_command('IlluminateToggle', require('illuminate').toggle, { bang = true }) 31 | vim.api.nvim_create_user_command('IlluminatePauseBuf', require('illuminate').pause_buf, { bang = true }) 32 | vim.api.nvim_create_user_command('IlluminateResumeBuf', require('illuminate').resume_buf, { bang = true }) 33 | vim.api.nvim_create_user_command('IlluminateToggleBuf', require('illuminate').toggle_buf, { bang = true }) 34 | vim.api.nvim_create_user_command('IlluminateDebug', require('illuminate').debug, { bang = true }) 35 | 36 | if not require('illuminate.config').disable_keymaps() then 37 | if not require('illuminate.util').has_keymap('n', '') then 38 | vim.keymap.set('n', '', require('illuminate').goto_next_reference, { desc = "Move to next reference" }) 39 | end 40 | if not require('illuminate.util').has_keymap('n', '') then 41 | vim.keymap.set('n', '', require('illuminate').goto_prev_reference, { desc = "Move to previous reference" }) 42 | end 43 | if not require('illuminate.util').has_keymap('o', '') then 44 | vim.keymap.set('o', '', require('illuminate').textobj_select) 45 | end 46 | if not require('illuminate.util').has_keymap('x', '') then 47 | vim.keymap.set('x', '', require('illuminate').textobj_select) 48 | end 49 | end 50 | EOF 51 | 52 | lua require('illuminate').set_highlight_defaults() 53 | augroup vim_illuminate_autocmds 54 | autocmd! 55 | autocmd ColorScheme * lua require('illuminate').set_highlight_defaults() 56 | augroup END 57 | 58 | finish 59 | end 60 | 61 | " Highlight group(s) {{{ 62 | if !hlexists('illuminatedWord') 63 | " this is for backwards compatibility 64 | if !empty(get(g:, 'Illuminate_hl_link', '')) 65 | exe get(g:, 'Illuminate_hl_link', '') 66 | else 67 | hi def link illuminatedWord cursorline 68 | endif 69 | endif 70 | " }}} 71 | 72 | " Autocommands {{{ 73 | if has('autocmd') 74 | augroup illuminated_autocmd 75 | autocmd! 76 | autocmd CursorMoved,InsertLeave * call illuminate#on_cursor_moved() 77 | autocmd WinLeave,BufLeave * call illuminate#on_leaving_autocmds() 78 | autocmd CursorMovedI * call illuminate#on_cursor_moved_i() 79 | autocmd InsertEnter * call illuminate#on_insert_entered() 80 | augroup END 81 | else 82 | echoerr 'Illuminate requires Vim compiled with +autocmd' 83 | finish 84 | endif 85 | " }}} 86 | 87 | " Commands {{{ 88 | command! -nargs=0 -bang IlluminationDisable call illuminate#disable_illumination(0) 89 | command! -nargs=0 -bang IlluminationEnable call illuminate#enable_illumination(0) 90 | command! -nargs=0 -bang IlluminationToggle call illuminate#toggle_illumination(0) 91 | " }}} Commands: 92 | 93 | " vim: foldlevel=1 foldmethod=marker 94 | --------------------------------------------------------------------------------