Skip to content

Instantly share code, notes, and snippets.

@jessedhillon
Created February 18, 2025 22:40
Show Gist options
  • Save jessedhillon/09c8ada83c09844815a574b9f0d83d2b to your computer and use it in GitHub Desktop.
Save jessedhillon/09c8ada83c09844815a574b9f0d83d2b to your computer and use it in GitHub Desktop.
preview-definition.nvim
-- 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