local M = {} local timers = {} local references = {} local paused_bufs = {} -- returns r1 < r2 based on start of range local function before_by_start(r1, r2) if r1['start'].line < r2['start'].line then return true end if r2['start'].line < r1['start'].line then return false end if r1['start'].character < r2['start'].character then return true end return false end -- returns r1 < r2 base on start and if they are disjoint local function before_disjoint(r1, r2) if r1['end'].line < r2['start'].line then return true end if r2['start'].line < r1['end'].line then return false end if r1['end'].character < r2['start'].character then return true end return false end -- check for cursor row in [start,end] -- check for cursor col in [start,end] -- While the end is technically exclusive based on the highlighting, we treat it as inclusive to match the server. local function point_in_range(point, range) if point.row == range['start']['line'] and point.col < range['start']['character'] then return false end if point.row == range['end']['line'] and point.col > range['end']['character'] then return false end return point.row >= range['start']['line'] and point.row <= range['end']['line'] end local function cursor_in_references(bufnr) if not references[bufnr] then return false end if vim.api.nvim_win_get_buf(0) ~= bufnr then return false end local crow, ccol = unpack(vim.api.nvim_win_get_cursor(0)) crow = crow - 1 -- reference ranges are (0,0)-indexed for (row,col) for _, reference in pairs(references[bufnr]) do local range = reference.range if point_in_range({ row = crow, col = ccol }, range) then return true end end return false end local function handle_document_highlight(result, bufnr, client_id) if not bufnr or not vim.api.nvim_buf_is_loaded(bufnr) then return end local btimer = timers[bufnr] if btimer then vim.loop.timer_stop(btimer) -- vim.loop.close(btimer) end if type(result) ~= 'table' then vim.lsp.util.buf_clear_references(bufnr) return end timers[bufnr] = vim.defer_fn(function() if not bufnr or not vim.api.nvim_buf_is_loaded(bufnr) then return end vim.lsp.util.buf_clear_references(bufnr) if cursor_in_references(bufnr) then local client = vim.lsp.get_client_by_id(client_id) if client then vim.lsp.util.buf_highlight_references(bufnr, result, client.offset_encoding) end end end, vim.g.Illuminate_delay or 17) table.sort(result, function(a, b) return before_by_start(a.range, b.range) end) references[bufnr] = result end local function valid(bufnr, range) return range and range.start.line < vim.api.nvim_buf_line_count(bufnr) and range.start.character < #vim.fn.getline(range.start.line + 1) end local function augroup(bufnr, autocmds) vim.cmd('augroup vim_illuminate_lsp' .. bufnr) vim.cmd('autocmd!') if autocmds then vim.b.illuminate_lsp_enabled = true autocmds() else vim.b.illuminate_lsp_enabled = false end vim.cmd('augroup END') end local function autocmd(bufnr) vim.cmd(string.format('autocmd CursorMoved,CursorMovedI lua require"illuminate".on_cursor_moved(%d)', bufnr, bufnr)) end local function move_cursor(row, col) if not paused_bufs[vim.api.nvim_get_current_buf()] then augroup(vim.api.nvim_get_current_buf(), function() vim.api.nvim_win_set_cursor(0, { row, col }) autocmd(vim.api.nvim_get_current_buf()) end) else vim.api.nvim_win_set_cursor(0, { row, col }) end end function M.on_attach(client) M.stop_buf() if client and not client.supports_method('textDocument/documentHighlight') then return end pcall(vim.api.nvim_command, 'IlluminationDisable!') augroup(vim.api.nvim_get_current_buf(), function() autocmd(vim.api.nvim_get_current_buf()) end) vim.lsp.handlers['textDocument/documentHighlight'] = function(...) if vim.fn.has('nvim-0.5.1') == 1 then handle_document_highlight(select(2, ...), select(3, ...).bufnr, select(3, ...).client_id) else handle_document_highlight(select(3, ...), select(5, ...), nil) end end vim.lsp.buf.document_highlight() end function M.on_cursor_moved(bufnr) if not cursor_in_references(bufnr) then vim.lsp.util.buf_clear_references(bufnr) end -- Best-effort check if any client support textDocument/documentHighlight local supported = nil -- For nvim 0.10+ if vim.lsp.get_clients then supported = false for _, client in ipairs(vim.lsp.get_clients({bufnr = bufnr})) do if client and client.supports_method('textDocument/documentHighlight') then supported = true end end -- For older versions elseif vim.lsp.for_each_buffer_client then supported = false vim.lsp.for_each_buffer_client(bufnr, function(client) if client.supports_method('textDocument/documentHighlight') then supported = true end end) end if supported == nil or supported then vim.lsp.buf.document_highlight() else augroup(vim.api.nvim_get_current_buf(), function() end) end end function M.get_document_highlights(bufnr) return references[bufnr] end function M.next_reference(opt) opt = vim.tbl_extend('force', { reverse = false, wrap = false, range_ordering = 'start', silent = false }, opt or {}) local before if opt.range_ordering == 'start' then before = before_by_start else before = before_disjoint end local bufnr = vim.api.nvim_get_current_buf() local refs = M.get_document_highlights(bufnr) if not refs or #refs == 0 then return nil end local next = nil local nexti = nil local crow, ccol = unpack(vim.api.nvim_win_get_cursor(0)) local crange = { start = { line = crow - 1, character = ccol } } for i, ref in ipairs(refs) do local range = ref.range if valid(bufnr, range) then if opt.reverse then if before(range, crange) and (not next or before(next, range)) then next = range nexti = i end else if before(crange, range) and (not next or before(range, next)) then next = range nexti = i end end end end if not next and opt.wrap then nexti = opt.reverse and #refs or 1 next = refs[nexti].range end if next then move_cursor(next.start.line + 1, next.start.character) if not opt.silent then print('[' .. nexti .. '/' .. #refs .. ']') end end return next end function M.toggle_pause() if paused_bufs[vim.api.nvim_get_current_buf()] then paused_bufs[vim.api.nvim_get_current_buf()] = false augroup(vim.api.nvim_get_current_buf(), function() autocmd(vim.api.nvim_get_current_buf()) end) M.on_cursor_moved(vim.api.nvim_get_current_buf()) else paused_bufs[vim.api.nvim_get_current_buf()] = true augroup(vim.api.nvim_get_current_buf(), nil) end end function M.configure(config) require('illuminate.config').set(config) end function M.pause() require('illuminate.engine').pause() end function M.resume() require('illuminate.engine').resume() end function M.toggle() require('illuminate.engine').toggle() end function M.toggle_buf() require('illuminate.engine').toggle_buf() end function M.pause_buf() require('illuminate.engine').pause_buf() end function M.stop_buf() require('illuminate.engine').stop_buf() end function M.resume_buf() require('illuminate.engine').resume_buf() end function M.freeze_buf() require('illuminate.engine').freeze_buf() end function M.unfreeze_buf() require('illuminate.engine').unfreeze_buf() end function M.toggle_freeze_buf() require('illuminate.engine').toggle_freeze_buf() end function M.invisible_buf() require('illuminate.engine').invisible_buf() end function M.visible_buf() require('illuminate.engine').visible_buf() end function M.toggle_visibility_buf() require('illuminate.engine').toggle_visibility_buf() end function M.goto_next_reference(wrap) if wrap == nil then wrap = vim.o.wrapscan end require('illuminate.goto').goto_next_reference(wrap) end function M.goto_prev_reference(wrap) if wrap == nil then wrap = vim.o.wrapscan end require('illuminate.goto').goto_prev_reference(wrap) end function M.textobj_select() require('illuminate.textobj').select() end function M.debug() require('illuminate.engine').debug() end function M.is_paused() return require('illuminate.engine').is_paused() end function M.set_highlight_defaults() vim.cmd [[ hi def IlluminatedWordText guifg=none guibg=none gui=underline hi def IlluminatedWordRead guifg=none guibg=none gui=underline hi def IlluminatedWordWrite guifg=none guibg=none gui=underline ]] end return M