├── .gitignore ├── LICENSE ├── README.md ├── docs └── readme │ └── compare1.png ├── lua └── stcursorword │ └── init.lua └── stylua.toml /.gitignore: -------------------------------------------------------------------------------- 1 | test.txt 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Trần Võ Sơn Tùng 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 | ## STCursorword 2 | 3 | A concise, precise, and high-performance cursor word highlighting plugin for Neovim, implemented in Lua. 4 | 5 | - ❓ [Reasons for creating](#reason) 6 | - 👀 [Installation](#installation) 7 | - 💻 [Configuration](#configuration) 8 | - 😆 [Usage](#usage) 9 | - 😁 [Contributing](#contributing) 10 | - ✌️ [License](#license) 11 | 12 | ## Reasons for creating this plugin 13 | 14 | 🎉 There are many plugins that do this, but none of them support disabling for certain filetypes. So, If I accidentally open a binary file such as .png file it gives me an error 15 | 16 | 🛠️ Other plugins always reset highlighting when the cursor moves, whereas this plugin does not. It only resets the highlighting when the cursor moves to a new word. 17 | 18 | For example, if the cursor is on the character `h` in the word `hello`, and you move the cursor to `e`, other plugins will remove the highlighting from the word `hello` and then highlight it again, even though the cursor is still on that word. Consequently, when you move to the `o`, it will undergo highlighting five times. 19 | 20 | 🍕 Easily to disable and enable when needed. 21 | 22 | 🚀 And a subjective reason is that I used to use the [nvim-cursorline](https://github.com/yamatsum/nvim-cursorline) plugin before, but I don't use the cursorline feature. Another plugins i found that [nvim-cursorword](https://github.com/xiyaowong/nvim-cursorword), it works quite similar to this plugin, but its setup method is not like the plugins I usually use. So this is the reason for this plugin. 23 | 24 | ## Preview 25 | 26 | https://github.com/sontungexpt/stcursorword/assets/92097639/2beb58cf-d6f9-44f9-979d-dea280a52b4d 27 | 28 | Compare with nvim cursorline 29 | 30 | Nvim cursorline error when accidentally open a binary file 31 | 32 | ![nvim-cursorline error](./docs/readme/compare1.png) 33 | 34 | This plugin fixed this error. 35 | 36 | https://github.com/sontungexpt/stcursorword/assets/92097639/d00be822-dfcf-47e8-8e97-a2a6ae0b6abf 37 | 38 | ## Installation 39 | 40 | ```lua 41 | -- lazy 42 | { 43 | "sontungexpt/stcursorword", 44 | event = "VeryLazy", 45 | config = true, 46 | }, 47 | ``` 48 | 49 | ## Configuration 50 | 51 | ```lua 52 | -- default configuration 53 | require("stcursorword").setup({ 54 | max_word_length = 100, -- if cursorword length > max_word_length then not highlight 55 | min_word_length = 2, -- if cursorword length < min_word_length then not highlight 56 | excluded = { 57 | filetypes = { 58 | "TelescopePrompt", 59 | }, 60 | buftypes = { 61 | -- "nofile", 62 | -- "terminal", 63 | }, 64 | patterns = { -- the pattern to match with the file path 65 | -- "%.png$", 66 | -- "%.jpg$", 67 | -- "%.jpeg$", 68 | -- "%.pdf$", 69 | -- "%.zip$", 70 | -- "%.tar$", 71 | -- "%.tar%.gz$", 72 | -- "%.tar%.xz$", 73 | -- "%.tar%.bz2$", 74 | -- "%.rar$", 75 | -- "%.7z$", 76 | -- "%.mp3$", 77 | -- "%.mp4$", 78 | }, 79 | }, 80 | highlight = { 81 | underline = true, 82 | fg = nil, 83 | bg = nil, 84 | }, 85 | }) 86 | ``` 87 | 88 | ## Usage 89 | 90 | `:Cursorword` command 91 | 92 | | **Args** | **Description** | 93 | | --------- | ------------------------------------------- | 94 | | `toggle` | Toggle highlight the word under the cursor | 95 | | `enable` | Enable highlight the word under the cursor | 96 | | `disable` | Disable highlight the word under the cursor | 97 | 98 | ## Contributing 99 | 100 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 101 | 102 | ## License 103 | 104 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 105 | -------------------------------------------------------------------------------- /docs/readme/compare1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sontungexpt/stcursorword/f6810ed5001eeee6061fdce77b292a5565823ded/docs/readme/compare1.png -------------------------------------------------------------------------------- /lua/stcursorword/init.lua: -------------------------------------------------------------------------------- 1 | local vim = vim 2 | local w, fn, api = vim.w, vim.fn, vim.api 3 | local hl, autocmd, get_line, get_cursor, matchstrpos, matchadd = 4 | api.nvim_set_hl, 5 | api.nvim_create_autocmd, 6 | api.nvim_get_current_line, 7 | api.nvim_win_get_cursor, 8 | fn.matchstrpos, 9 | fn.matchadd 10 | 11 | local PLUG_NAME = "stcursorword" 12 | local enabled = false 13 | local prev_line = -1 -- The previous line number where the cursor was found 14 | local prev_start_column = math.huge -- The previous start column position of the word found 15 | local prev_end_column = -1 -- The previous end column position of the word found 16 | 17 | local M = {} 18 | 19 | local default_configs = { 20 | max_word_length = 100, 21 | min_word_length = 2, 22 | excluded = { 23 | filetypes = {}, 24 | buftypes = { 25 | "prompt", 26 | "terminal", 27 | -- "nofile", 28 | }, 29 | patterns = { 30 | -- "%.png$", 31 | -- "%.jpg$", 32 | -- "%.jpeg$", 33 | -- "%.pdf$", 34 | -- "%.zip$", 35 | -- "%.tar$", 36 | -- "%.tar%.gz$", 37 | -- "%.tar%.xz$", 38 | -- "%.tar%.bz2$", 39 | -- "%.rar$", 40 | -- "%.7z$", 41 | -- "%.mp3$", 42 | -- "%.mp4$", 43 | }, 44 | }, 45 | highlight = { 46 | underline = true, 47 | }, 48 | } 49 | 50 | local function merge_config(default_opts, user_opts) 51 | local default_options_type = type(default_opts) 52 | 53 | if default_options_type == type(user_opts) then 54 | if default_options_type == "table" and default_opts[1] == nil then 55 | for k, v in pairs(user_opts) do 56 | default_opts[k] = merge_config(default_opts[k], v) 57 | end 58 | else 59 | default_opts = user_opts 60 | end 61 | elseif default_opts == nil then 62 | default_opts = user_opts 63 | end 64 | return default_opts 65 | end 66 | 67 | local matchdelete = function() 68 | if w.stcursorword ~= nil then 69 | pcall(fn.matchdelete, w.stcursorword) 70 | w.stcursorword = nil 71 | prev_start_column = math.huge 72 | prev_end_column = -1 73 | end 74 | end 75 | 76 | local highlight_same = function(configs) 77 | if not enabled then return end 78 | 79 | local cursor_pos = get_cursor(0) 80 | local cursor_column = cursor_pos[2] 81 | local cursor_line = cursor_pos[1] 82 | 83 | -- if cusor doesn't move out of the word, do nothing 84 | if 85 | prev_line == cursor_line 86 | and cursor_column >= prev_start_column 87 | and cursor_column < prev_end_column 88 | then 89 | return 90 | end 91 | prev_line = cursor_line 92 | 93 | -- clear old match 94 | matchdelete() 95 | 96 | local line = get_line() 97 | 98 | -- Fixes vim:E976 error when cursor is on a blob 99 | if fn.type(line) == vim.v.t_blob then return end 100 | 101 | -- get the left part of the word containing the cursor 102 | local matches = matchstrpos(line:sub(1, cursor_column + 1), [[\w*$]]) 103 | local word = matches[1] -- left part of the word 104 | 105 | if word ~= "" then 106 | prev_start_column = matches[2] 107 | -- get the right part of the word not containing the cursor 108 | matches = matchstrpos(line, [[^\w*]], cursor_column + 1) 109 | word = word .. matches[1] -- combine with right part of the word 110 | prev_end_column = matches[3] 111 | 112 | local word_len = #word 113 | if word_len < configs.min_word_length or word_len > configs.max_word_length then return end 114 | 115 | w.stcursorword = 116 | matchadd(PLUG_NAME, [[\(\<\|\W\|\s\)\zs]] .. word .. [[\ze\(\s\|[^[:alnum:]_]\|$\)]], -1) 117 | end 118 | end 119 | 120 | local arr_contains = function(tbl, value) 121 | for _, v in ipairs(tbl) do 122 | if v == value then return true end 123 | end 124 | return false 125 | end 126 | 127 | local matches_file_patterns = function(file_name, file_patterns) 128 | for _, pattern in ipairs(file_patterns) do 129 | if file_name:match(pattern) then return true end 130 | end 131 | return false 132 | end 133 | 134 | local check_disabled = function(excluded, bufnr) 135 | return arr_contains(excluded.buftypes, api.nvim_get_option_value("buftype", { buf = bufnr or 0 })) 136 | or arr_contains(excluded.filetypes, api.nvim_get_option_value("filetype", { buf = bufnr or 0 })) 137 | or matches_file_patterns(api.nvim_buf_get_name(bufnr or 0), excluded.patterns) 138 | end 139 | 140 | local enable = function(configs) 141 | enabled = true 142 | -- initial when plugin is loaded 143 | local group = api.nvim_create_augroup(PLUG_NAME, { clear = true }) 144 | hl(0, PLUG_NAME, configs.highlight) 145 | 146 | local disabled = check_disabled(configs.excluded, 0) 147 | if not disabled then highlight_same(configs) end -- initial match 148 | 149 | autocmd("ColorScheme", { 150 | group = group, 151 | callback = function() hl(0, PLUG_NAME, configs.highlight) end, 152 | }) 153 | 154 | local skip_cursormoved = false 155 | 156 | autocmd({ "BufEnter", "WinEnter" }, { 157 | group = group, 158 | callback = function() 159 | -- Wait for 8ms to ensure the buffer is fully loaded to avoid errors. 160 | -- If the buffer is not fully loaded: 161 | -- - The current line is 0. 162 | -- - The buffer type (buftype) is nil. 163 | -- - The file type (filetype) is nil. 164 | skip_cursormoved = true 165 | vim.defer_fn(function() 166 | disabled = check_disabled(configs.excluded, 0) 167 | if not disabled then highlight_same(configs) end 168 | end, 8) 169 | end, 170 | }) 171 | 172 | autocmd({ "CursorMoved", "CursorMovedI" }, { 173 | group = group, 174 | callback = function() 175 | if skip_cursormoved then 176 | skip_cursormoved = false 177 | elseif not disabled then 178 | highlight_same(configs) 179 | end 180 | end, 181 | }) 182 | 183 | autocmd({ "BufLeave", "WinLeave" }, { 184 | group = group, 185 | callback = matchdelete, 186 | }) 187 | end 188 | 189 | local disable = function() 190 | matchdelete() 191 | api.nvim_del_augroup_by_name(PLUG_NAME) 192 | enabled = false 193 | end 194 | 195 | local toggle = function(configs) 196 | if enabled then 197 | disable() 198 | else 199 | enable(configs) 200 | end 201 | end 202 | 203 | M.setup = function(user_opts) 204 | local opts = merge_config(default_configs, user_opts) 205 | api.nvim_create_user_command("Cursorword", function(args) 206 | local arg = string.lower(args.args) 207 | if arg == "enable" then 208 | enable(opts) 209 | elseif arg == "disable" then 210 | disable() 211 | elseif arg == "toggle" then 212 | toggle(opts) 213 | end 214 | end, { 215 | nargs = 1, 216 | complete = function() return { "enable", "disable", "toggle" } end, 217 | desc = "Enable or disable cursorword", 218 | }) 219 | enable(opts) 220 | end 221 | 222 | return M 223 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | column_width = 105 2 | indent_type = "Tabs" 3 | indent_width = 2 4 | quote_style = "AutoPreferDouble" 5 | call_parentheses = "NoSingleTable" 6 | collapse_simple_statement = "Always" 7 | --------------------------------------------------------------------------------