Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,11 @@ require("quicker").setup({
-- Keep the cursor to the right of the filename and lnum columns
constrain_cursor = true,
highlight = {
-- Use treesitter highlighting
-- Attach treesitter parser to qf buffer, highlight text in real time as you edit.
treesitter = true,
-- Do not register callbacks when buffer line counts exceed this limit. In other words,
-- highlight won't get updated as you edit.
max_lines = 10000,
-- Use LSP semantic token highlighting
lsp = true,
-- Load the referenced buffers to apply more accurate highlights (may be slow)
Expand Down
7 changes: 6 additions & 1 deletion lua/quicker/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ local default_config = {
opts = {
buflisted = false,
number = false,
concealcursor = "nvic",
relativenumber = false,
signcolumn = "auto",
winfixheight = true,
Expand All @@ -26,8 +27,11 @@ local default_config = {
-- Keep the cursor to the right of the filename and lnum columns
constrain_cursor = true,
highlight = {
-- Use treesitter highlighting
-- Attach treesitter parser to qf buffer, highlight text in real time as you edit.
treesitter = true,
-- Do not register callbacks when buffer line counts exceed this limit. In other words,
-- highlight won't get updated as you edit.
max_lines = 10000,
-- Use LSP semantic token highlighting
lsp = true,
-- Load the referenced buffers to apply more accurate highlights (may be slow)
Expand Down Expand Up @@ -155,6 +159,7 @@ end

---@class (exact) quicker.HighlightConfig
---@field treesitter boolean
---@field max_lines integer
---@field lsp boolean
---@field load_buffers boolean

Expand Down
101 changes: 65 additions & 36 deletions lua/quicker/display.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ local util = require("quicker.util")

local M = {}

local EM_QUAD = " "
-- A EM_QUAD and a space, we include an extra space before the real text, because it
-- avoids strange cases such as no highlight when insert at start of qf text.
local EM_QUAD = "  "
local EM_QUAD_LEN = EM_QUAD:len()
M.EM_QUAD = EM_QUAD
M.EM_QUAD_LEN = EM_QUAD_LEN
Expand Down Expand Up @@ -216,27 +218,6 @@ local function add_item_highlights_from_buf(qfbufnr, item, line, lnum)
offset = offset - item_space
end

-- Add treesitter highlights
if config.highlight.treesitter then
for _, hl in ipairs(highlight.buf_get_ts_highlights(item.bufnr, item.lnum)) do
local start_col, end_col, hl_group = hl[1], hl[2], hl[3]
if end_col == -1 then
end_col = src_line:len()
end
-- If the highlight starts at the beginning of the source line, then it might be off the
-- buffer in the quickfix because we've removed leading whitespace. If so, clamp the value
-- to 0. Except, for some reason 0 gives incorrect results, but -1 works properly even
-- though -1 should indicate the *end* of the line. Not sure why this work, but it does.
local hl_start = math.max(-1, start_col + offset)
vim.api.nvim_buf_set_extmark(qfbufnr, ns, lnum - 1, hl_start, {
hl_group = hl_group,
end_col = end_col + offset,
priority = 100,
strict = false,
})
end
end

-- Add LSP semantic token highlights
if config.highlight.lsp then
for _, hl in ipairs(highlight.buf_get_lsp_highlights(item.bufnr, item.lnum)) do
Expand Down Expand Up @@ -267,6 +248,10 @@ local function highlight_buffer_when_entered(qfbufnr, info)
vim.b[qfbufnr].pending_highlight = nil
info.start_idx = 1
info.end_idx = vim.api.nvim_buf_line_count(qfbufnr)
info.regions = {}
info.region_count = 0
info.empty_regions = {}
info.ft = {}
schedule_highlights(info)
end,
})
Expand Down Expand Up @@ -339,23 +324,44 @@ add_qf_highlights = function(info)
loaded = true
end

if loaded then
add_item_highlights_from_buf(qfbufnr, item, line, i)
elseif config.highlight.treesitter then
local ft = info.ft[item.bufnr]
if ft == nil then
ft = loaded and vim.bo[item.bufnr].filetype or vim.filetype.match({ buf = item.bufnr })
info.ft[item.bufnr] = ft
end

if config.highlight.treesitter and ft then
info.regions[ft] = info.regions[ft] or {}
info.region_count = info.region_count + 1
info.empty_regions[ft] = info.empty_regions[ft] or {}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is empty_regions for?

Copy link
Author

@xzbdmw xzbdmw Feb 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

empty_regions is used to to clear the included regions and make sure a reparse with clean state will happen after Refresh, if you modify the saved file and call Refresh on qf the previous highlight are still there:

iShot_2025-02-16_09.58.15.mp4

Copy link
Author

@xzbdmw xzbdmw Feb 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit: this doesn't work, we have to manually include an empty region, multiple call to vim.treesitter.get_parser with the same argument returns the same parser.

If we want to support max_lines option, then empty_region is not needed, because lines are computed using regions, it complicates the logic of when should we register the callback (vim.treesitter.get_parser) and when should not (vim.treesitter.languagetree.new), now I'm just creating a new parser each time attach is called.

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,
priority = 100,
strict = false,

-- Note: we must include the new line character, when multiple lines are treated as one
-- range, without "\n" causes all the problems, for example:
--
-- tests/tmp/expand_1.lua ┃ 2┃-- a comment
-- tests/tmp/expand_1.lua ┃ 3┃local a = 1
--
-- The parser will treat this as "-- a commentlocal a = 1".
local region = {
i - 1,
filename:len() + EM_QUAD_LEN - 1, -- skip EM_QUAD but include a space
i,
0,
}
info.previous_item = info.previous_item or item
if info.previous_item.bufnr == item.bufnr and info.previous_item.lnum == item.lnum - 1 then
-- Merge current range with previous one, parse them together.
table.insert(info.regions[ft][#info.regions[ft]], region)
else
table.insert(info.regions[ft], {
region,
})
end
info.previous_item = item
end
if loaded then
add_item_highlights_from_buf(qfbufnr, item, line, i)
end
end

Expand Down Expand Up @@ -383,6 +389,12 @@ add_qf_highlights = function(info)
return
end
end
if config.highlight.treesitter then
local register_cbs = info.region_count <= config.highlight.max_lines
-- cleanup previous regions each time we call setqflist.
require("quicker.treesitter").attach(qf_list.qfbufnr, info.empty_regions, register_cbs)
require("quicker.treesitter").attach(qf_list.qfbufnr, info.regions, register_cbs)
end

vim.api.nvim_buf_clear_namespace(qf_list.qfbufnr, ns, info.end_idx, -1)
end
Expand Down Expand Up @@ -425,6 +437,11 @@ end
---@field id integer
---@field start_idx integer
---@field end_idx integer
---@field regions table<string, Range4[][]>
---@field region_count integer
---@field empty_regions table<string, Range4[][]>
---@field ft table<integer, string|nil>
---@field previous_item QuickFixItem
---@field winid integer
---@field quickfix 1|0
---@field force_bufload? boolean field injected by us to control if we're forcing a bufload for the syntax highlighting
Expand Down Expand Up @@ -561,6 +578,10 @@ function M.quickfixtextfunc(info)
virt_text_pos = "inline",
invalidate = true,
})
vim.api.nvim_buf_set_extmark(qf_list.qfbufnr, ns, lnum - 1, end_col, {
end_col = end_col + EM_QUAD_LEN - 1,
conceal = "",
})
idmap[id] = lnum

-- Highlight the filename
Expand All @@ -581,13 +602,21 @@ function M.quickfixtextfunc(info)
virt_lines = { header },
virt_lines_above = true,
})
-- vim.api.nvim_buf_set_extmark(qf_list.qfbufnr, ns, lnum - 1, end_col, {
-- end_col = end_col + EM_QUAD_LEN - 1,
-- conceal = "",
-- })
end
end
end
vim.schedule(set_virt_text)

-- If we just rendered the last item, add highlights
if info.end_idx == #items then
info.regions = {}
info.region_count = 0
info.empty_regions = {}
info.ft = {}
schedule_highlights(info)

if qf_list.qfbufnr > 0 then
Expand Down
109 changes: 0 additions & 109 deletions lua/quicker/highlight.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,71 +19,6 @@ local function get_highlight_query(lang)
end
end

---@param bufnr integer
---@param lnum integer
---@return quicker.TSHighlight[]
function M.buf_get_ts_highlights(bufnr, lnum)
local filetype = vim.bo[bufnr].filetype
if not filetype or filetype == "" then
filetype = vim.filetype.match({ buf = bufnr }) or ""
end
local lang = vim.treesitter.language.get_lang(filetype) or filetype
if lang == "" then
return {}
end
local ok, parser = pcall(vim.treesitter.get_parser, bufnr, lang)
if not ok or not parser then
return {}
end

local row = lnum - 1
if not parser:is_valid() then
parser:parse(true)
end

local highlights = {}
parser:for_each_tree(function(tstree, tree)
if not tstree then
return
end

local root_node = tstree:root()
local root_start_row, _, root_end_row, _ = root_node:range()

-- Only worry about trees within the line range
if root_start_row > row or root_end_row < row then
return
end

local query = get_highlight_query(tree:lang())

-- Some injected languages may not have highlight queries.
if not query then
return
end

for capture, node, metadata in query:iter_captures(root_node, bufnr, row, root_end_row + 1) do
if capture == nil then
break
end

local range = vim.treesitter.get_range(node, bufnr, metadata[capture])
local start_row, start_col, _, end_row, end_col, _ = unpack(range)
if start_row > row then
break
end
local capture_name = query.captures[capture]
local hl = string.format("@%s.%s", capture_name, tree:lang())
if end_row > start_row then
end_col = -1
end
table.insert(highlights, { start_col, end_col, hl })
end
end)

return highlights
end

---@class quicker.LSPHighlight
---@field [1] integer start_col
---@field [2] integer end_col
Expand Down Expand Up @@ -157,50 +92,6 @@ function M.buf_get_lsp_highlights(bufnr, lnum)
return lsp_highlights
end

---@param item QuickFixItem
---@param line string
---@return quicker.TSHighlight[]
M.get_heuristic_ts_highlights = function(item, line)
local filetype = vim.filetype.match({ buf = item.bufnr })
if not filetype then
return {}
end

local lang = vim.treesitter.language.get_lang(filetype)
if not lang then
return {}
end

local has_parser, parser = pcall(vim.treesitter.get_string_parser, line, lang)
if not has_parser then
return {}
end

local root = parser:parse(true)[1]:root()
local query = vim.treesitter.query.get(lang, "highlights")
if not query then
return {}
end

local highlights = {}
for capture, node, metadata in query:iter_captures(root, line) do
if capture == nil then
break
end

local range = vim.treesitter.get_range(node, line, metadata[capture])
local start_row, start_col, _, end_row, end_col, _ = unpack(range)
local capture_name = query.captures[capture]
local hl = string.format("@%s.%s", capture_name, lang)
if end_row > start_row then
end_col = -1
end
table.insert(highlights, { start_col, end_col, hl })
end

return highlights
end

function M.set_highlight_groups()
if vim.tbl_isempty(vim.api.nvim_get_hl(0, { name = "QuickFixHeaderHard" })) then
vim.api.nvim_set_hl(0, "QuickFixHeaderHard", { link = "Delimiter", default = true })
Expand Down
Loading