Created
February 18, 2025 22:40
-
-
Save jessedhillon/09c8ada83c09844815a574b9f0d83d2b to your computer and use it in GitHub Desktop.
preview-definition.nvim
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- Function to display a custom selection menu | |
local function select_definition(definitions, callback) | |
local api = vim.api | |
-- Create a scratch buffer for the menu | |
local buf = api.nvim_create_buf(false, true) | |
-- Populate the buffer with definition options | |
local lines = {} | |
for i, def in ipairs(definitions) do | |
local uri = def.uri or def.targetUri | |
local fname = vim.uri_to_fname(uri) | |
local range = def.range or def.targetRange | |
local line = range.start.line + 1 -- Convert to 1-based indexing | |
-- Read the file and extract the specific line | |
local file_lines = vim.fn.readfile(fname) | |
local line_content = file_lines[line] or "[not available]" | |
-- Strip leading whitespace from the line content | |
line_content = line_content:match("^%s*(.*)$") or line_content | |
-- Build the unformatted string | |
local formatted_line = string.format("%s:%d %s", fname, line, line_content) | |
table.insert(lines, formatted_line) | |
end | |
-- Add lines to the buffer | |
api.nvim_buf_set_lines(buf, 0, -1, false, lines) | |
-- Apply highlighting to each line | |
local max_length = 0 | |
for i, def in ipairs(definitions) do | |
local uri = def.uri or def.targetUri | |
local range = def.range or def.targetRange | |
local fname = vim.uri_to_fname(uri):match("^%s*(.-)%s*$") | |
local line = lines[i]:match("^%s*(.-)%s*$") | |
local fname_start = string.find(line, fname) or 1 | |
local fname_end = fname_start + #vim.uri_to_fname(uri or "") - 1 | |
local range_start = fname_end + 1 | |
local range_end = range_start + #tostring(range.start.line + 1) + #tostring(range.start.character) | |
-- Highlight file name (fname) | |
api.nvim_buf_add_highlight(buf, -1, "Directory", i - 1, fname_start - 1, fname_end) | |
-- Highlight line number and column (line:col) | |
api.nvim_buf_add_highlight(buf, -1, "CursorLineNr", i - 1, range_start, range_end) | |
max_length = math.max(max_length, #lines[i]) | |
end | |
-- api.nvim_buf_set_lines(buf, 0, -1, false, lines) | |
-- Set buffer options | |
api.nvim_set_option_value("modifiable", false, { buf = buf }) | |
api.nvim_set_option_value("bufhidden", "wipe", { buf = buf }) | |
-- Map navigation keys and selection | |
api.nvim_buf_set_keymap(buf, "n", "<CR>", ":lua SelectDefinition()<CR>", { noremap = true, silent = true }) | |
api.nvim_buf_set_keymap(buf, "n", "q", ":close<CR>", { noremap = true, silent = true }) | |
api.nvim_buf_set_keymap(buf, "n", "<Esc>", ":close<CR>", { noremap = true, silent = true }) | |
-- Set up the floating window | |
local max_width = math.floor(vim.o.columns * 0.8) | |
local win_width = math.min(max_width, max_length) | |
local win_height = #definitions -- Include padding | |
local win_row = math.floor((vim.o.lines - win_height) / 2) | |
local win_col = math.floor((vim.o.columns - win_width) / 2) | |
local win = api.nvim_open_win(buf, true, { | |
title = "Select a location", | |
relative = "cursor", | |
width = win_width, | |
height = win_height, | |
row = 1, -- win_row, | |
col = 1, -- win_col, | |
border = "rounded", | |
style = "minimal", | |
}) | |
-- Save definitions for selection handler | |
_G.SelectDefinition = function() | |
local line = vim.fn.line(".") | |
if line <= #definitions then | |
local selected = definitions[line] | |
api.nvim_win_close(win, true) | |
callback(selected) | |
else | |
vim.notify("Invalid selection", vim.log.levels.WARN) | |
end | |
end | |
end | |
-- Function to handle jumping to a location in the source window | |
local function jump_to_location(buf, cursor_pos) | |
local source_win = vim.api.nvim_get_current_win() -- Get the window we opened from | |
vim.api.nvim_set_current_win(source_win) -- Switch back to the source window | |
vim.cmd("e " .. buf) -- Open the file | |
vim.api.nvim_win_set_cursor(source_win, cursor_pos) -- Move cursor to the target position | |
end | |
-- Function to open a new tab at the location | |
local function open_tab_at_location(buf, cursor_pos) | |
vim.cmd("tabnew " .. buf) -- Open a new tab with the file | |
local new_win = vim.api.nvim_get_current_win() -- Get the new tab's window | |
vim.api.nvim_win_set_cursor(new_win, cursor_pos) -- Move cursor to the target position | |
end | |
-- Add key mappings for Enter and 't' in the preview window | |
local function add_preview_keymaps(preview_win, target_file, target_cursor) | |
vim.api.nvim_buf_set_keymap(preview_win, "n", "<CR>", "", { | |
noremap = true, | |
silent = true, | |
callback = function() | |
jump_to_location(target_file, target_cursor) | |
end, | |
}) | |
vim.api.nvim_buf_set_keymap(preview_win, "n", "t", "", { | |
noremap = true, | |
silent = true, | |
callback = function() | |
open_tab_at_location(target_file, target_cursor) | |
end, | |
}) | |
end | |
-- Function to open a modal for a specific location | |
local function open_preview(location) | |
local api = vim.api | |
local uri = location.uri or location.targetUri | |
local range = location.range or location.targetRange | |
local fname = vim.uri_to_fname(uri) | |
local row = range.start.line | |
local col = range.start.character | |
-- Read the file content | |
local lines = vim.fn.readfile(fname) | |
-- Create a scratch buffer for the content | |
local buf = api.nvim_create_buf(false, true) | |
api.nvim_buf_set_lines(buf, 0, -1, false, lines) | |
-- Set buffer options | |
api.nvim_set_option_value("buftype", "nofile", { buf = buf }) | |
api.nvim_set_option_value("modifiable", false, { buf = buf }) | |
api.nvim_set_option_value("readonly", true, { buf = buf }) | |
-- Enable syntax highlighting | |
local ft = vim.filetype.match({ filename = fname }) | |
if ft then | |
api.nvim_set_option_value("filetype", ft, { buf = buf }) | |
end | |
-- Determine floating window dimensions | |
local win_width = math.min(vim.fn.winwidth(0), math.floor(vim.o.columns * 0.8)) | |
local win_height = math.floor(vim.o.lines * 0.8) | |
local win_row = 0.25 * math.floor(win_height) / 2 -- math.floor((vim.o.lines - win_height) / 2) | |
local win_col = math.floor((vim.o.columns - win_width) / 2) | |
-- Create the floating window | |
local win = api.nvim_open_win(buf, true, { | |
relative = "cursor", | |
width = win_width, | |
height = win_height, | |
row = win_row, | |
col = 1, -- win_col, | |
border = "rounded", | |
style = "minimal", | |
title = fname, | |
}) | |
api.nvim_set_option_value("cursorline", true, { win = win }) | |
api.nvim_set_option_value("number", true, { win = win }) | |
api.nvim_set_option_value("relativenumber", false, { win = win }) | |
api.nvim_set_option_value("winblend", 4, { win = win }) | |
-- Scroll to the definition location | |
local offset_from_top = 6 -- Desired number of lines from the top | |
local scroll_to_line = math.max(row + 1 - offset_from_top, 1) -- Ensure we don't scroll before the first line | |
api.nvim_win_set_cursor(win, { row + 1, col }) -- Move the cursor to the desired position | |
api.nvim_win_call(win, function() | |
vim.cmd("normal! zt") -- Scroll the window so the cursor is at the top | |
vim.cmd(string.format("normal! %d<C-y>", offset_from_top)) -- Adjust the scroll to position the line offset_from_top lines from the top | |
end) | |
-- Close modal on Escape | |
api.nvim_buf_set_keymap(buf, "n", "<Esc>", ":close<CR>", { noremap = true, silent = true }) | |
add_preview_keymaps(buf, fname, { row + 1, col }) | |
end | |
-- Main function | |
local function preview_definition() | |
local lsp = vim.lsp | |
-- Request the definition location | |
lsp.buf_request(0, "textDocument/definition", lsp.util.make_position_params(), function(err, result, ctx, _) | |
if err then | |
vim.notify("Error fetching definition: " .. err.message, vim.log.levels.ERROR) | |
return | |
end | |
if not result or vim.tbl_isempty(result) then | |
vim.notify("No definition found.", vim.log.levels.INFO) | |
return | |
end | |
-- Handle multiple definitions with a custom popup | |
if #result > 1 then | |
select_definition(result, function(selected_location) | |
open_preview(selected_location) | |
end) | |
return | |
end | |
-- Single result case | |
open_preview(result[1]) | |
end) | |
end | |
-- Bind <leader>pd to the main function | |
vim.keymap.set("n", "<leader>pd", preview_definition, { | |
noremap = true, | |
silent = true, | |
desc = "[P]review [D]efinition", | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment