refactor(config): inline location reference plugin and refresh docs

This commit is contained in:
Paul B. Kim 2026-03-06 12:27:42 +09:00
parent ef32c150ff
commit cbfd797e56
No known key found for this signature in database
5 changed files with 263 additions and 314 deletions

View File

@ -4,7 +4,7 @@
- Root entrypoint: `init.lua` (Kickstart-based primary config).
- Custom Lua modules live under `lua/custom/`.
- Plugin specs are split into `lua/custom/plugins/*.lua` (one concern per file, e.g. `gitsigns.lua`, `lint.lua`, `persistence.lua`).
- Utility modules (non-plugin config) live in `lua/custom/*.lua` (for example `wrapping.lua`, `health.lua`).
- Utility modules (non-plugin config) live in `lua/custom/*.lua` (for example `wrapping.lua`).
- Reference docs live in `README.md` and `doc/kickstart.txt`.
- Plugin versions are pinned in `lazy-lock.json`.

View File

@ -44,15 +44,20 @@ nvim --headless "+MasonToolsInstallSync" "+qa"
### Git workflow
- `<leader>gg`: open Neogit UI
- `<leader>gd`: open Diffview
- `<leader>gD`: close Diffview
- `<leader>gf`: Diffview file history (current file)
- `<leader>gF`: Diffview repo history
- `<leader>Gg`: open Neogit UI
- `<leader>Gd`: open Diffview
- `<leader>GD`: close Diffview
- `<leader>Gf`: Diffview file history (current file)
- `<leader>GF`: Diffview repo history
- `<leader>h...`: Gitsigns hunk actions (`:which-key <leader>h`)
### Diagnostics and code navigation
- `<leader>gd`: goto definition
- `<leader>gD`: goto declaration
- `<leader>gr`: goto references
- `<leader>gi`: goto implementation
- `<leader>gt`: goto type definition
- `<leader>xx`: Trouble diagnostics
- `<leader>xw`: Trouble workspace diagnostics
- `<leader>xd`: Trouble current buffer diagnostics
@ -64,6 +69,13 @@ nvim --headless "+MasonToolsInstallSync" "+qa"
- `<leader>jc` / `<leader>jC`: next/previous class start
- Textobject select (operator-pending/visual): `af`/`if` for function, `ac`/`ic` for class
### Yank references (AI-friendly)
- `<leader>ya`: yank absolute reference with location and symbol
- `<leader>yr`: yank relative reference with location and symbol
- `<leader>yf`: yank current buffer file as `@relative/path`
- `<leader>yd`: yank current buffer parent directory as `@relative/dir`
### Tests and debug
- `<leader>nr`: neotest run nearest

View File

@ -1,52 +0,0 @@
--[[
--
-- This file is not required for your own configuration,
-- but helps people determine if their system is setup correctly.
--
--]]
local check_version = function()
local verstr = tostring(vim.version())
if not vim.version.ge then
vim.health.error(string.format("Neovim out of date: '%s'. Upgrade to latest stable or nightly", verstr))
return
end
if vim.version.ge(vim.version(), '0.10-dev') then
vim.health.ok(string.format("Neovim version is: '%s'", verstr))
else
vim.health.error(string.format("Neovim out of date: '%s'. Upgrade to latest stable or nightly", verstr))
end
end
local check_external_reqs = function()
-- Basic utils: `git`, `make`, `unzip`
for _, exe in ipairs { 'git', 'make', 'unzip', 'rg' } do
local is_executable = vim.fn.executable(exe) == 1
if is_executable then
vim.health.ok(string.format("Found executable: '%s'", exe))
else
vim.health.warn(string.format("Could not find executable: '%s'", exe))
end
end
return true
end
return {
check = function()
vim.health.start 'kickstart.nvim'
vim.health.info [[NOTE: Not every warning is a 'must-fix' in `:checkhealth`
Fix only warnings for plugins and languages you intend to use.
Mason will give warnings for languages that are not installed.
You do not need to install, unless you want to use those languages!]]
local uv = vim.uv or vim.loop
vim.health.info('System Information: ' .. vim.inspect(uv.os_uname()))
check_version()
check_external_reqs()
end,
}

View File

@ -1,244 +0,0 @@
local M = {}
local sk = vim.lsp.protocol.SymbolKind
local symbol_kinds = {
[sk.Class] = true,
[sk.Method] = true,
[sk.Constructor] = true,
[sk.Enum] = true,
[sk.Interface] = true,
[sk.Function] = true,
[sk.Struct] = true,
}
local function range_contains(range, line0, col0)
if not range then
return false
end
local start_line = range.start.line
local start_col = range.start.character
local end_line = range['end'].line
local end_col = range['end'].character
if line0 < start_line or line0 > end_line then
return false
end
if line0 == start_line and col0 < start_col then
return false
end
if line0 == end_line and col0 > end_col then
return false
end
return true
end
local function range_size(range)
-- Weight lines heavily so a single-line range is always smaller than a multi-line one
return (range['end'].line - range.start.line) * 100000 + (range['end'].character - range.start.character)
end
local function append_document_symbols(symbols, out, parent_name)
for _, symbol in ipairs(symbols or {}) do
local symbol_name = symbol.name
local full_name = parent_name and (parent_name .. '.' .. symbol_name) or symbol_name
table.insert(out, {
name = full_name,
kind = symbol.kind,
range = symbol.range,
})
append_document_symbols(symbol.children, out, full_name)
end
end
local function append_symbol_information(symbols, out)
for _, symbol in ipairs(symbols or {}) do
local name = symbol.name
if symbol.containerName and symbol.containerName ~= '' then
name = symbol.containerName .. '.' .. name
end
table.insert(out, {
name = name,
kind = symbol.kind,
range = symbol.location and symbol.location.range,
})
end
end
local function get_symbol_at_cursor()
if #vim.lsp.get_clients { bufnr = 0 } == 0 then
return nil
end
local params = { textDocument = vim.lsp.util.make_text_document_params() }
local responses = vim.lsp.buf_request_sync(0, 'textDocument/documentSymbol', params, 500)
if not responses then
return nil
end
local cursor = vim.api.nvim_win_get_cursor(0)
local line0 = cursor[1] - 1
-- LSP ranges use UTF-16 offsets; convert from byte offset for correct matching
-- on files with non-ASCII characters (e.g. CJK, emoji).
local line_text = vim.api.nvim_buf_get_lines(0, line0, line0 + 1, false)[1] or ''
local col0 = vim.str_utfindex(line_text, 'utf-16', cursor[2])
local symbols = {}
for _, response in pairs(responses) do
if not response.error then
local result = response.result
if type(result) == 'table' and result[1] then
if result[1].selectionRange or result[1].children then
append_document_symbols(result, symbols, nil)
else
append_symbol_information(result, symbols)
end
end
end
end
local best_symbol, best_size
for _, symbol in ipairs(symbols) do
if symbol_kinds[symbol.kind] and range_contains(symbol.range, line0, col0) then
local size = range_size(symbol.range)
if not best_size or size < best_size then
best_size = size
best_symbol = symbol.name
end
end
end
return best_symbol
end
local function get_relative_path_from_root(file_path)
local root = vim.fs.root(file_path, { '.git' }) or vim.fn.getcwd()
if vim.fs.relpath then
local rel = vim.fs.relpath(root, file_path)
if rel then
return rel
end
end
local prefix = root
if not prefix:match '/$' then
prefix = prefix .. '/'
end
if file_path:sub(1, #prefix) == prefix then
return file_path:sub(#prefix + 1)
end
return file_path
end
local function get_file_parts()
local file_path = vim.api.nvim_buf_get_name(0)
if file_path == '' then
vim.notify('No file path for current buffer', vim.log.levels.WARN)
return nil
end
return {
file_path = file_path,
file_rel = get_relative_path_from_root(file_path),
}
end
local function get_location_parts()
local parts = get_file_parts()
if not parts then
return nil
end
local mode = vim.api.nvim_get_mode().mode
local loc
if mode == 'v' or mode == 'V' or mode == '\22' then
-- Use 'v' (visual anchor) and '.' (cursor) for the live selection.
-- '</'> marks are only reliable after exiting visual mode (e.g. ':' mappings),
-- but <Cmd> mappings stay in visual mode so those marks are stale.
local start_pos = vim.fn.getpos 'v'
local end_pos = vim.fn.getpos '.'
local s_line, s_col = start_pos[2], start_pos[3]
local e_line, e_col = end_pos[2], end_pos[3]
-- getpos('v') returns zeros when called outside a real visual context; fall back.
if s_line == 0 then
local cur = vim.api.nvim_win_get_cursor(0)
s_line, s_col = cur[1], cur[2] + 1
e_line, e_col = s_line, s_col
elseif s_line > e_line or (s_line == e_line and s_col > e_col) then
s_line, e_line = e_line, s_line
s_col, e_col = e_col, s_col
end
loc = string.format('%d:%d-%d:%d', s_line, s_col, e_line, e_col)
else
local cursor = vim.api.nvim_win_get_cursor(0)
local line_num = cursor[1]
local line_text = vim.api.nvim_buf_get_lines(0, line_num - 1, line_num, false)[1] or ''
local end_col = math.max(vim.fn.strchars(line_text), 1)
loc = string.format('%d:%d-%d:%d', line_num, 1, line_num, end_col)
end
return {
file_path = parts.file_path,
file_rel = parts.file_rel,
loc = loc,
symbol = get_symbol_at_cursor(),
}
end
local function yank(label, value)
vim.fn.setreg('"', value)
pcall(vim.fn.setreg, '+', value)
vim.notify('Copied ' .. label .. ': ' .. value, vim.log.levels.INFO)
end
function M.copy_absolute_location_reference()
local parts = get_location_parts()
if not parts then
return
end
local location = string.format('%s:%s', parts.file_path, parts.loc)
if parts.symbol and parts.symbol ~= '' then
location = location .. ' symbol:' .. parts.symbol
end
yank('absolute location', location)
end
function M.copy_relative_location_reference()
local parts = get_location_parts()
if not parts then
return
end
local symbol = parts.symbol and parts.symbol ~= '' and parts.symbol or 'none'
yank('relative location', string.format('@%s loc:%s symbol:%s', parts.file_rel, parts.loc, symbol))
end
function M.copy_buffer_file_reference()
local parts = get_file_parts()
if not parts then
return
end
yank('buffer file', string.format('@%s', parts.file_rel))
end
function M.copy_buffer_directory_reference()
local parts = get_file_parts()
if not parts then
return
end
local abs_dir = vim.fn.fnamemodify(parts.file_path, ':h')
local rel_dir = get_relative_path_from_root(abs_dir)
yank('buffer directory', string.format('@%s', rel_dir))
end
return M

View File

@ -1,35 +1,268 @@
local sk = vim.lsp.protocol.SymbolKind
local symbol_kinds = {
[sk.Class] = true,
[sk.Method] = true,
[sk.Constructor] = true,
[sk.Enum] = true,
[sk.Interface] = true,
[sk.Function] = true,
[sk.Struct] = true,
}
local function range_contains(range, line0, col0)
if not range then
return false
end
local start_line = range.start.line
local start_col = range.start.character
local end_line = range['end'].line
local end_col = range['end'].character
if line0 < start_line or line0 > end_line then
return false
end
if line0 == start_line and col0 < start_col then
return false
end
if line0 == end_line and col0 > end_col then
return false
end
return true
end
local function range_size(range)
-- Weight lines heavily so a single-line range is always smaller than a multi-line one
return (range['end'].line - range.start.line) * 100000 + (range['end'].character - range.start.character)
end
local function append_document_symbols(symbols, out, parent_name)
for _, symbol in ipairs(symbols or {}) do
local symbol_name = symbol.name
local full_name = parent_name and (parent_name .. '.' .. symbol_name) or symbol_name
table.insert(out, {
name = full_name,
kind = symbol.kind,
range = symbol.range,
})
append_document_symbols(symbol.children, out, full_name)
end
end
local function append_symbol_information(symbols, out)
for _, symbol in ipairs(symbols or {}) do
local name = symbol.name
if symbol.containerName and symbol.containerName ~= '' then
name = symbol.containerName .. '.' .. name
end
table.insert(out, {
name = name,
kind = symbol.kind,
range = symbol.location and symbol.location.range,
})
end
end
local function get_symbol_at_cursor()
if #vim.lsp.get_clients { bufnr = 0 } == 0 then
return nil
end
local params = { textDocument = vim.lsp.util.make_text_document_params() }
local responses = vim.lsp.buf_request_sync(0, 'textDocument/documentSymbol', params, 500)
if not responses then
return nil
end
local cursor = vim.api.nvim_win_get_cursor(0)
local line0 = cursor[1] - 1
-- LSP ranges use UTF-16 offsets; convert from byte offset for correct matching
-- on files with non-ASCII characters (e.g. CJK, emoji).
local line_text = vim.api.nvim_buf_get_lines(0, line0, line0 + 1, false)[1] or ''
local col0 = vim.str_utfindex(line_text, 'utf-16', cursor[2])
local symbols = {}
for _, response in pairs(responses) do
if not response.error then
local result = response.result
if type(result) == 'table' and result[1] then
if result[1].selectionRange or result[1].children then
append_document_symbols(result, symbols, nil)
else
append_symbol_information(result, symbols)
end
end
end
end
local best_symbol, best_size
for _, symbol in ipairs(symbols) do
if symbol_kinds[symbol.kind] and range_contains(symbol.range, line0, col0) then
local size = range_size(symbol.range)
if not best_size or size < best_size then
best_size = size
best_symbol = symbol.name
end
end
end
return best_symbol
end
local function get_relative_path_from_root(file_path)
local root = vim.fs.root(file_path, { '.git' }) or vim.fn.getcwd()
if vim.fs.relpath then
local rel = vim.fs.relpath(root, file_path)
if rel then
return rel
end
end
local prefix = root
if not prefix:match '/$' then
prefix = prefix .. '/'
end
if file_path:sub(1, #prefix) == prefix then
return file_path:sub(#prefix + 1)
end
return file_path
end
local function get_file_parts()
local file_path = vim.api.nvim_buf_get_name(0)
if file_path == '' then
vim.notify('No file path for current buffer', vim.log.levels.WARN)
return nil
end
return {
file_path = file_path,
file_rel = get_relative_path_from_root(file_path),
}
end
local function get_location_parts()
local parts = get_file_parts()
if not parts then
return nil
end
local mode = vim.api.nvim_get_mode().mode
local loc
if mode == 'v' or mode == 'V' or mode == '\22' then
-- Use 'v' (visual anchor) and '.' (cursor) for the live selection.
-- '</'> marks are only reliable after exiting visual mode (e.g. ':' mappings),
-- but <Cmd> mappings stay in visual mode so those marks are stale.
local start_pos = vim.fn.getpos 'v'
local end_pos = vim.fn.getpos '.'
local s_line, s_col = start_pos[2], start_pos[3]
local e_line, e_col = end_pos[2], end_pos[3]
-- getpos('v') returns zeros when called outside a real visual context; fall back.
if s_line == 0 then
local cur = vim.api.nvim_win_get_cursor(0)
s_line, s_col = cur[1], cur[2] + 1
e_line, e_col = s_line, s_col
elseif s_line > e_line or (s_line == e_line and s_col > e_col) then
s_line, e_line = e_line, s_line
s_col, e_col = e_col, s_col
end
loc = string.format('%d:%d-%d:%d', s_line, s_col, e_line, e_col)
else
local cursor = vim.api.nvim_win_get_cursor(0)
local line_num = cursor[1]
local line_text = vim.api.nvim_buf_get_lines(0, line_num - 1, line_num, false)[1] or ''
local end_col = math.max(vim.fn.strchars(line_text), 1)
loc = string.format('%d:%d-%d:%d', line_num, 1, line_num, end_col)
end
return {
file_path = parts.file_path,
file_rel = parts.file_rel,
loc = loc,
symbol = get_symbol_at_cursor(),
}
end
local function yank(label, value)
vim.fn.setreg('"', value)
pcall(vim.fn.setreg, '+', value)
vim.notify('Copied ' .. label .. ': ' .. value, vim.log.levels.INFO)
end
local function copy_absolute_location_reference()
local parts = get_location_parts()
if not parts then
return
end
local location = string.format('%s:%s', parts.file_path, parts.loc)
if parts.symbol and parts.symbol ~= '' then
location = location .. ' symbol:' .. parts.symbol
end
yank('absolute location', location)
end
local function copy_relative_location_reference()
local parts = get_location_parts()
if not parts then
return
end
local symbol = parts.symbol and parts.symbol ~= '' and parts.symbol or 'none'
yank('relative location', string.format('@%s loc:%s symbol:%s', parts.file_rel, parts.loc, symbol))
end
local function copy_buffer_file_reference()
local parts = get_file_parts()
if not parts then
return
end
yank('buffer file', string.format('@%s', parts.file_rel))
end
local function copy_buffer_directory_reference()
local parts = get_file_parts()
if not parts then
return
end
local abs_dir = vim.fn.fnamemodify(parts.file_path, ':h')
local rel_dir = get_relative_path_from_root(abs_dir)
yank('buffer directory', string.format('@%s', rel_dir))
end
return {
'folke/which-key.nvim',
keys = {
{
'<leader>ya',
function()
require('custom.location_reference').copy_absolute_location_reference()
end,
copy_absolute_location_reference,
mode = { 'n', 'x' },
desc = '[Y]ank [A]bsolute ref',
},
{
'<leader>yr',
function()
require('custom.location_reference').copy_relative_location_reference()
end,
copy_relative_location_reference,
mode = { 'n', 'x' },
desc = '[Y]ank [R]elative ref',
},
{
'<leader>yf',
function()
require('custom.location_reference').copy_buffer_file_reference()
end,
copy_buffer_file_reference,
mode = { 'n', 'x' },
desc = '[Y]ank Buffer [F]ile',
},
{
'<leader>yd',
function()
require('custom.location_reference').copy_buffer_directory_reference()
end,
copy_buffer_directory_reference,
mode = { 'n', 'x' },
desc = '[Y]ank Buffer [D]irectory',
},