Skip to content

Commit

Permalink
refactor: put the filename back into the real buffer text
Browse files Browse the repository at this point in the history
  • Loading branch information
stevearc committed Dec 22, 2024
1 parent 1d9ba5b commit ab0d0d8
Show file tree
Hide file tree
Showing 26 changed files with 259 additions and 125 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ require("quicker").setup({
-- Set to "unmodified" to only write unmodified buffers.
autosave = "unmodified",
},
-- Keep the cursor to the right of the filename and lnum columns
constrain_cursor = true,
highlight = {
-- Use treesitter highlighting
treesitter = true,
Expand Down
2 changes: 2 additions & 0 deletions doc/quicker.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ OPTIONS *quicker-option
-- Set to "unmodified" to only write unmodified buffers.
autosave = "unmodified",
},
-- Keep the cursor to the right of the filename and lnum columns
constrain_cursor = true,
highlight = {
-- Use treesitter highlighting
treesitter = true,
Expand Down
4 changes: 4 additions & 0 deletions lua/quicker/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ local default_config = {
-- Set to "unmodified" to only write unmodified buffers.
autosave = "unmodified",
},
-- Keep the cursor to the right of the filename and lnum columns
constrain_cursor = true,
highlight = {
-- Use treesitter highlighting
treesitter = true,
Expand Down Expand Up @@ -68,6 +70,7 @@ local default_config = {
---@field opts table<string, any>
---@field keys quicker.Keymap[]
---@field use_default_opts boolean
---@field constrain_cursor boolean
---@field highlight quicker.HighlightConfig
---@field edit quicker.EditConfig
---@field type_icons table<string, string>
Expand All @@ -82,6 +85,7 @@ local M = {}
---@field opts? table<string, any> Local options to set for quickfix
---@field keys? quicker.Keymap[] Keymaps to set for the quickfix buffer
---@field use_default_opts? boolean Set to false to disable the default options in `opts`
---@field constrain_cursor? boolean Keep the cursor to the right of the filename and lnum columns
---@field highlight? quicker.SetupHighlightConfig Configure syntax highlighting
---@field edit? quicker.SetupEditConfig
---@field type_icons? table<string, string> Map of quickfix item type to icon
Expand Down
44 changes: 44 additions & 0 deletions lua/quicker/cursor.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
local M = {}

local function constrain_cursor()
local display = require("quicker.display")
local cur = vim.api.nvim_win_get_cursor(0)
local line = vim.api.nvim_buf_get_lines(0, cur[1] - 1, cur[1], true)[1]
local idx = line:find(display.EM_QUAD, 1, true)
if not idx then
return
end
local min_col = idx + display.EM_QUAD_LEN - 1
if cur[2] < min_col then
vim.api.nvim_win_set_cursor(0, { cur[1], min_col })
end
end

---@param bufnr number
function M.constrain_cursor(bufnr)
-- HACK: we have to defer this call because sometimes the autocmds don't take effect.
vim.schedule(function()
if not vim.api.nvim_buf_is_valid(bufnr) then
return
end
local aug = vim.api.nvim_create_augroup("quicker", { clear = false })
vim.api.nvim_create_autocmd("InsertEnter", {
desc = "Constrain quickfix cursor position",
group = aug,
nested = true,
buffer = bufnr,
-- For some reason the cursor bounces back to its original position,
-- so we have to defer the call
callback = vim.schedule_wrap(constrain_cursor),
})
vim.api.nvim_create_autocmd({ "CursorMoved", "ModeChanged" }, {
desc = "Constrain quickfix cursor position",
nested = true,
group = aug,
buffer = bufnr,
callback = constrain_cursor,
})
end)
end

return M
77 changes: 51 additions & 26 deletions lua/quicker/display.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ local util = require("quicker.util")

local M = {}

local EM_QUAD = ""
local EM_QUAD_LEN = EM_QUAD:len()
M.EM_QUAD = EM_QUAD
M.EM_QUAD_LEN = EM_QUAD_LEN

---@class (exact) QuickFixUserData
---@field header? "hard"|"soft" When present, this line is a header
---@field lnum? integer Encode the lnum separately for valid=0 items
Expand Down Expand Up @@ -75,7 +80,7 @@ local function get_cached_qf_col_width(id, items)
max_len = math.max(max_len, vim.api.nvim_strwidth(M.get_filename_from_item(item)))
end

cached = { max_len + 1, #items }
cached = { max_len, #items }
_col_width_cache[id] = cached
end
return cached[1]
Expand Down Expand Up @@ -197,7 +202,7 @@ local function add_item_highlights_from_buf(qfbufnr, item, line, lnum)

-- Only add highlights if the text in the quickfix matches the source line
if item.text:sub(item_space + 1) == src_line:sub(src_space + 1) then
local offset = 0
local offset = line:find(EM_QUAD, 1, true) + EM_QUAD_LEN - 1
local prefix = prefixes[item.bufnr]
if type(prefix) == "string" then
-- Since prefixes get deserialized from vim.b, if there are holes in the map they get
Expand Down Expand Up @@ -332,8 +337,13 @@ add_qf_highlights = function(info)
if loaded then
add_item_highlights_from_buf(qfbufnr, item, line, i)
elseif config.highlight.treesitter then
for _, hl in ipairs(highlight.get_heuristic_ts_highlights(item, line)) do
local filename = vim.split(line, EM_QUAD, { plain = true })[1]
local offset = filename:len() + EM_QUAD_LEN
local text = line:sub(offset + 1)
for _, hl in ipairs(highlight.get_heuristic_ts_highlights(item, text)) do
local start_col, end_col, hl_group = hl[1], hl[2], hl[3]
start_col = start_col + offset
end_col = end_col + offset
vim.api.nvim_buf_set_extmark(qfbufnr, ns, i - 1, start_col, {
hl_group = hl_group,
end_col = end_col,
Expand Down Expand Up @@ -428,6 +438,7 @@ function M.quickfixtextfunc(info)
local b = config.borders
local qf_list = load_qf(info, true)
local locations = {}
local invalid_filenames = {}
local headers = {}
local ret = {}
local items = qf_list.items
Expand All @@ -444,7 +455,7 @@ function M.quickfixtextfunc(info)
if user_data.header == "hard" then
-- Header when expanded QF list
local pieces = {
string.rep(b.strong_header, col_width),
string.rep(b.strong_header, col_width + 1),
b.strong_cross,
string.rep(b.strong_header, lnum_width),
}
Expand All @@ -459,7 +470,7 @@ function M.quickfixtextfunc(info)
elseif user_data.header == "soft" then
-- Soft header when expanded QF list
local pieces = {
string.rep(b.soft_header, col_width),
string.rep(b.soft_header, col_width + 1),
b.soft_cross,
string.rep(b.soft_header, lnum_width),
}
Expand All @@ -477,41 +488,33 @@ function M.quickfixtextfunc(info)
if item.valid == 1 then
-- Matching line
local lnum = item.lnum == 0 and " " or item.lnum
local filename = rpad(M.get_filename_from_item(item), col_width)
table.insert(locations, {
{ rpad(M.get_filename_from_item(item), col_width), "QuickFixFilename" },
{ b.vert, "Delimiter" },
{ lnum_fmt:format(lnum), "QuickFixLineNr" },
{ b.vert, "Delimiter" },
})
table.insert(ret, remove_prefix(item.text, prefixes[item.bufnr]))
table.insert(ret, filename .. EM_QUAD .. remove_prefix(item.text, prefixes[item.bufnr]))
elseif user_data.lnum then
-- Non-matching line from quicker.nvim context lines
local filename = string.rep(" ", col_width)
table.insert(locations, {
{ string.rep(" ", col_width), "" },
{ b.vert, "Delimiter" },
{ lnum_fmt:format(user_data.lnum), "QuickFixLineNr" },
{ b.vert, "Delimiter" },
})
table.insert(ret, remove_prefix(item.text, prefixes[item.bufnr]))
table.insert(ret, filename .. EM_QUAD .. remove_prefix(item.text, prefixes[item.bufnr]))
else
-- Other non-matching line
local lnum = item.lnum == 0 and " " or item.lnum
local filename = rpad(M.get_filename_from_item(item), col_width)
table.insert(locations, {
{ rpad(M.get_filename_from_item(item), col_width), "QuickFixFilenameInvalid" },
{ b.vert, "Delimiter" },
{ lnum_fmt:format(lnum), "QuickFixLineNr" },
{ b.vert, "Delimiter" },
})
table.insert(ret, remove_prefix(item.text, prefixes[item.bufnr]))
end
end

-- If we just rendered the last item, add highlights
if info.end_idx == #items then
schedule_highlights(info)

if qf_list.qfbufnr > 0 then
vim.b[qf_list.qfbufnr].qf_prefixes = prefixes
invalid_filenames[#locations] = true
table.insert(ret, filename .. EM_QUAD .. remove_prefix(item.text, prefixes[item.bufnr]))
end
end

Expand All @@ -530,21 +533,34 @@ function M.quickfixtextfunc(info)

local ns = vim.api.nvim_create_namespace("quicker_locations")
vim.api.nvim_buf_clear_namespace(qf_list.qfbufnr, ns, start_idx - 1, -1)
local header_ns = vim.api.nvim_create_namespace("quicker_headers")
vim.api.nvim_buf_clear_namespace(qf_list.qfbufnr, header_ns, start_idx - 1, -1)
local filename_ns = vim.api.nvim_create_namespace("quicker_filenames")
vim.api.nvim_buf_clear_namespace(qf_list.qfbufnr, filename_ns, start_idx - 1, -1)

local idmap = {}
for i, loc in ipairs(locations) do
local lnum = start_idx + i - 1
local id = vim.api.nvim_buf_set_extmark(qf_list.qfbufnr, ns, lnum - 1, 0, {
local id =
vim.api.nvim_buf_set_extmark(qf_list.qfbufnr, ns, lnum - 1, col_width + EM_QUAD_LEN, {
right_gravity = false,
virt_text = loc,
virt_text_pos = "inline",
invalidate = true,
})
idmap[id] = lnum

-- Highlight the filename
vim.api.nvim_buf_set_extmark(qf_list.qfbufnr, filename_ns, lnum - 1, 0, {
hl_group = invalid_filenames[i] and "QuickFixFilenameInvalid" or "QuickFixFilename",
right_gravity = false,
virt_text = loc,
virt_text_pos = "inline",
end_col = col_width,
priority = 100,
invalidate = true,
})
idmap[id] = lnum
end
vim.b[qf_list.qfbufnr].qf_ext_id_to_item_idx = idmap

local header_ns = vim.api.nvim_create_namespace("quicker_headers")
vim.api.nvim_buf_clear_namespace(qf_list.qfbufnr, header_ns, start_idx - 1, -1)
for _, pair in ipairs(headers) do
local i, header = pair[1], pair[2]
local lnum = start_idx + i - 1
Expand All @@ -557,6 +573,15 @@ function M.quickfixtextfunc(info)
end
vim.schedule(set_virt_text)

-- If we just rendered the last item, add highlights
if info.end_idx == #items then
schedule_highlights(info)

if qf_list.qfbufnr > 0 then
vim.b[qf_list.qfbufnr].qf_prefixes = prefixes
end
end

return ret
end

Expand Down
62 changes: 55 additions & 7 deletions lua/quicker/editor.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local config = require("quicker.config")
local display = require("quicker.display")
local util = require("quicker.util")
local M = {}

Expand All @@ -21,6 +22,36 @@ local function plural(n, base, pluralized)
end
end

---Replace the text in a quickfix line, preserving the lineno virt text
---@param bufnr integer
---@param lnum integer
---@param new_text string
local function replace_qf_line(bufnr, lnum, new_text)
local old_line = vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, true)[1]

local old_idx = old_line:find(display.EM_QUAD, 1, true)
local new_idx = new_text:find(display.EM_QUAD, 1, true)

-- If we're missing the em quad delimiter in either the old or new text, the best we can do is
-- replace the whole line
if not old_idx or not new_idx then
vim.api.nvim_buf_set_text(bufnr, lnum - 1, 0, lnum - 1, -1, { new_text })
return
end

-- Replace first the text after the em quad, then the filename before.
-- This keeps the line number virtual text in the same location.
vim.api.nvim_buf_set_text(
bufnr,
lnum - 1,
old_idx + display.EM_QUAD_LEN - 1,
lnum - 1,
-1,
{ new_text:sub(new_idx + display.EM_QUAD_LEN) }
)
vim.api.nvim_buf_set_text(bufnr, lnum - 1, 0, lnum - 1, old_idx, { new_text:sub(1, new_idx) })
end

---@param bufnr integer
---@param lnum integer
---@param text string
Expand Down Expand Up @@ -164,24 +195,41 @@ local function save_changes(bufnr, loclist_win)
return
end

-- Trim the filename off of the line
local idx = string.find(line, display.EM_QUAD, 1, true)
if not idx then
add_qf_error(
bufnr,
i,
"The delimiter between filename and text has been deleted. Undo, delete line, or :Refresh.",
"DiagnosticError"
)
if winid then
vim.api.nvim_win_set_cursor(winid, { i, 0 })
end
exit_early = true
return
end
local text = line:sub(idx + display.EM_QUAD_LEN)

local item = qf_list.items[found_idx]
if item.bufnr ~= 0 and item.lnum ~= 0 then
if not vim.api.nvim_buf_is_loaded(item.bufnr) then
vim.fn.bufload(item.bufnr)
end
-- add the whitespace prefix back to the parsed line text
line = (prefixes[item.bufnr] or "") .. line
text = (prefixes[item.bufnr] or "") .. text

local src_line = vim.api.nvim_buf_get_lines(item.bufnr, item.lnum - 1, item.lnum, false)[1]
if src_line and line ~= src_line then
if line:gsub("^%s*", "") == src_line:gsub("^%s*", "") then
if src_line and text ~= src_line then
if text:gsub("^%s*", "") == src_line:gsub("^%s*", "") then
-- If they only disagree in their leading whitespace, just take the changes after the
-- whitespace and assume that the whitespace hasn't changed
line = src_line:match("^%s*") .. line:gsub("^%s*", "")
text = src_line:match("^%s*") .. text:gsub("^%s*", "")
end
end

local text_edit, err = get_text_edit(item, line, src_line)
local text_edit, err = get_text_edit(item, text, src_line)
if text_edit then
local chng_err = add_change(item.bufnr, text_edit)
if chng_err then
Expand All @@ -200,7 +248,7 @@ local function save_changes(bufnr, loclist_win)
end

-- add item to future qflist
item.text = line
item.text = text
table.insert(new_items, item)
end)()
if exit_early then
Expand Down Expand Up @@ -293,7 +341,7 @@ local function save_changes(bufnr, loclist_win)
vim.schedule(function()
-- Mark the lines with changes that could not be applied
for lnum, new_text in pairs(errors) do
vim.api.nvim_buf_set_text(bufnr, lnum - 1, 0, lnum - 1, -1, { new_text })
replace_qf_line(bufnr, lnum, new_text)
local item = new_items[lnum]
local src_line = vim.api.nvim_buf_get_lines(item.bufnr, item.lnum - 1, item.lnum, false)[1]
add_qf_error(bufnr, lnum, src_line)
Expand Down
4 changes: 4 additions & 0 deletions lua/quicker/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ local function setup(opts)
desc = "Update the quickfix list with the current buffer text for each item",
})

if config.constrain_cursor then
require("quicker.cursor").constrain_cursor(args.buf)
end

config.on_qf(args.buf)
end,
})
Expand Down
5 changes: 4 additions & 1 deletion tests/editor_spec.lua
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
local config = require("quicker.config")
local display = require("quicker.display")
local quicker = require("quicker")
local test_util = require("tests.test_util")

---@param lnum integer
---@param line string
local function replace_text(lnum, line)
vim.api.nvim_buf_set_text(0, lnum - 1, 0, lnum - 1, -1, { line })
local prev_line = vim.api.nvim_buf_get_lines(0, lnum - 1, lnum, true)[1]
local idx = prev_line:find(display.EM_QUAD, 1, true)
vim.api.nvim_buf_set_text(0, lnum - 1, idx + display.EM_QUAD_LEN - 1, lnum - 1, -1, { line })
end

---@param lnum integer
Expand Down
Loading

0 comments on commit ab0d0d8

Please sign in to comment.