Skip to content

Instantly share code, notes, and snippets.

@bfrg
Last active November 27, 2022 00:45
Show Gist options
  • Save bfrg/93b7be7d9d04d561baa70cff6e16477a to your computer and use it in GitHub Desktop.
Save bfrg/93b7be7d9d04d561baa70cff6e16477a to your computer and use it in GitHub Desktop.
Highlight quickfix (and/or location-list) errors as virtual text in the buffer

Highlight quickfix errors as virtual text

This script will display the errors in the current quickfix and/or location list as virtual text in the buffer.

Virtual text can be aligned after, right, below or above the line containing the error.

NOTE: For a full plugin, see vim-qf-diagnostics.

Commands

  • :VirtualText [args] — Highlight current quickfix items as virtual text
  • :LVirtualText [args] — Highlight current location-list items as virtual text
  • :VirtualTextClear — Remove virtual-text of current quickfix list
  • :LVirtualTextClear — Remove virtual-text of current location list
  • :VirtualTextToggle [args] — Toggle virtual-text highlightings of current quickfix list
  • :LVirtualTextToggle [args] — Toggle virtual-text highlightings of current location list

[args] is optional and can be: after, right, below, above.

Screenshots

virtual text: right screenshot-1

virtual text: after screenshot-2

virtual text: below screenshot-3

vim9script
# Display the errors in the current quickfix or location list as virtual text in
# the buffer.
#
# The virtual text can be aligned 'after', 'right', 'below' or 'above' the line
# containing the error.
#
# Commands:
#
# :VirtualText [args] -- Highlight current quickfix items as virtual text
# :LVirtualText [args] -- Highlight current location-list items as virtual text
#
# :VirtualTextClear -- Remove virtual-text of current quickfix list
# :LVirtualTextClear -- Remove virtual-text of current location list
#
# :VirtualTextToggle [args] -- Toggle virtual-text highlightings of current quickfix list
# :LVirtualTextToggle [args] -- Toggle virtual-text highlightings of current location list
#
# [args] is optional and can be: after, right, below, above
#
hi QfVirtualError ctermfg=204 ctermbg=52 guifg=#E06C75 guibg=#3A1F22
hi QfVirtualWarning ctermfg=180 ctermbg=136 guifg=#D19A66 guibg=#7D5C3D
hi QfVirtualInfo ctermfg=114 ctermbg=22 guifg=#98C379 guibg=#223A1F
hi QfVirtualNormal ctermfg=39 ctermbg=17 guifg=#61AFEF guibg=#1F223A
hi link QfVirtualNote QfVirtualInfo
prop_type_delete('qf-virt-error')
prop_type_delete('qf-virt-warning')
prop_type_delete('qf-virt-info')
prop_type_delete('qf-virt-note')
prop_type_delete('qf-virt-normal')
prop_type_add('qf-virt-error', {highlight: 'QfVirtualError'})
prop_type_add('qf-virt-warning', {highlight: 'QfVirtualWarning'})
prop_type_add('qf-virt-info', {highlight: 'QfVirtualInfo'})
prop_type_add('qf-virt-note', {highlight: 'QfVirtualNote'})
prop_type_add('qf-virt-normal', {highlight: 'QfVirtualNormal'})
const virttype: dict<string> = {
E: 'qf-virt-error',
W: 'qf-virt-warning',
I: 'qf-virt-info',
N: 'qf-virt-note',
'': 'qf-virt-normal',
}
const prefix: string = '■ '
# Data structure for storing quickfix items and location-list items, accessed
# with 0 and win-ID, respectively:
#
# {
# 0: {
# id: 2,
# changedtick: 4,
# text_align: 'after',
# items: [ getqflist()->filter() … ],
# buffers: {
# bufnr_1: [0, 1, 2],
# bufnr_2: [3],
# bufnr_3: [4, 5],
# …
# },
# ids: {
# bufnr_1: [-1, -2, -3],
# bufnr_2: [-1],
# bufnr_3: [-1, -2],
# …
# },
# },
# 1001: {…}
# }
#
# Quickfix list (accessed with '0'):
#
# qfs[0].id -- quickfix attribute 'id'
# qfs[0].changedtick -- quickfix attribute 'changedtick'
# qfs[0].text_align -- how to align virtual text
# qfs[0].items[bufnr] -- quickfix items (as returned by getqflist())
# qfs[0].buffers[bufnr] -- indices to quickfix items, grouped by bufnr
# qfs[0].ids[bufnr] -- text-property IDs as returned by prop_add()
#
# Location-lists (accessed with win-ID, use Group_id(true) below):
#
# qfs[1004].id …
# … same as for quickfix list
#
var qfs: dict<any> = {}
def g:Debug(): dict<any>
return qfs
enddef
# Return the group-ID of either quickfix or location list. A quickfix-list has
# group-ID '0', a location-list has a group-ID equal to the window-ID it belongs
# to.
def Group_id(loclist: bool): number
if loclist
return win_getid()->getwininfo()[0].loclist
? getloclist(0, {filewinid: 0}).filewinid
: win_getid()
endif
return 0
enddef
# Group quickfix list 'items' by buffer number
def Buffer_groups(items: list<dict<any>>): dict<list<number>>
final buffers: dict<list<number>> = {}
for [idx: number, item: dict<any>] in items(items)
if !has_key(buffers, item.bufnr)
buffers[item.bufnr] = []
endif
add(buffers[item.bufnr], idx)
endfor
return buffers
enddef
# Add text-properties to buffer 'bufnr' using the items stored in 'group'
# Note: 'bufnr' must be displayed in a window
# TODO should we use getbufinfo(bufnr)[0].linecount instead of line('$', winid)?
def Props_add(bufnr: number, group: number)
const maxlnum: number = line('$', win_findbuf(bufnr)[0])
const text_align: string = qfs[group].text_align
var virtid: number
for idx in qfs[group].buffers[bufnr]
final item: dict<any> = qfs[group].items[idx]
# Sanity check for item.lnum (should we use try/catch?)
if item.lnum > maxlnum
continue
endif
virtid = prop_add(item.lnum, 0, {
type: get(virttype, toupper(item.type), virttype['']),
bufnr: bufnr,
text: prefix .. item.text->split('\n')[0]->trim(),
text_align: text_align,
text_padding_left: text_align == 'below' || text_align == 'above' ? indent(item.lnum) : 2,
})
# Save the text-prop ID for later when removing virtual text
add(qfs[group].ids[bufnr], virtid)
endfor
enddef
def Props_remove(group: number)
if !has_key(qfs, group)
return
endif
# TODO should we set an autocmd that removes bufnr in qfs[group].ids and
# qfs[group].buffers when a buffer is wiped out instead of checking with
# bufexists()?
var bufnr: number
for i in keys(qfs[group].ids)
bufnr = str2nr(i)
if !bufexists(bufnr)
continue
endif
for id in qfs[group].ids[i]
prop_remove({
id: id,
bufnr: bufnr,
types: ['qf-virt-error', 'qf-virt-warning', 'qf-virt-info', 'qf-virt-note', 'qf-virt-normal'],
both: true,
all: true
})
endfor
endfor
remove(qfs, group)
if empty(qfs)
autocmd_delete([
{group: 'qf-text-props', event: 'BufWinEnter'},
{group: 'qf-text-props', event: 'BufReadPost'}
])
endif
enddef
# Re-apply text-properties to a buffer that was reloaded with ':edit'. Since we
# are postponing adding text-properties until the buffer is displayed in a
# window, we return when the buffer isn't displayed in a window. For example,
# when we call bufload(bufnr), BufRead is triggered but we don't want to add
# text-properties until BufWinEnter is triggered.
def On_bufread()
const bufnr: number = expand('<abuf>')->str2nr()
const wins: list<number> = win_findbuf(bufnr)
if empty(wins)
return
endif
for group in keys(qfs)
if has_key(qfs[group].ids, bufnr)
Props_add(bufnr, str2nr(group))
endif
endfor
enddef
# We add text-properties to the buffer only after it's displayed in a window
#
# TODO:
# - Should we check quickfix's 'changedtick', and if it changed, get new
# quickfix list, re-group items, and delete old text-properties before adding
# new ones?
# - Check if quickfix list with given quickfix ID still exists, if it doesn't,
# we need to remove ALL text-properties, and don't add new ones
# - Should we cache quickfix items or retrieve them with getqflist() on every
# BufWinEnter (and BufReadPost)?
def On_bufwinenter()
const bufnr: number = expand('<abuf>')->str2nr()
for group in keys(qfs)
# If no text-property IDs saved for a buffer, virtual text hasn't been
# added to the buffer yet
if has_key(qfs[group].ids, bufnr) && empty(qfs[group].ids[bufnr])
Props_add(bufnr, str2nr(group))
endif
endfor
enddef
# When a window is closed, remove all text-properties added from a location
# list, and delete all data stored for that window
def On_winclosed()
const winid: number = expand('<amatch>')->str2nr()
Props_remove(winid)
enddef
def Virtualtext_add(loclist: bool, align: string)
const group: number = Group_id(loclist)
const xlist: dict<any> = loclist
? getloclist(0, {items: 0, id: 0, changedtick: 0})
: getqflist({items: 0, id: 0, changedtick: 0})
# First remove previously placed virtual-text
Props_remove(group)
if empty(xlist.items)
return
endif
const text_align: string = align ?? 'after'
var idx: number = -1
var virtid: number
var wins: list<number>
qfs->extend({
[group]: {
id: xlist.id,
changedtick: xlist.changedtick,
items: xlist.items,
text_align: text_align,
buffers: {},
ids: {}
}
})
final buffers: dict<list<number>> = qfs[group].buffers
final ids: dict<list<number>> = qfs[group].ids
for i in xlist.items
++idx
if i.lnum < 1 || !i.valid || i.bufnr < 1 || !bufexists(i.bufnr)
continue
endif
# Group quickfix items by buffer numbers
if !has_key(buffers, i.bufnr)
buffers[i.bufnr] = []
ids[i.bufnr] = []
endif
add(buffers[i.bufnr], idx)
# Add text-property only if buffer is displayed in a window. If not, we
# add it later using an autocommand when BufWinEnter is triggered
wins = win_findbuf(i.bufnr)
if empty(wins) || i.lnum > line('$', wins[0])
continue
endif
# TODO should we wrap prop_add() inside try/catch instead of checking
# the max lnum in the if-condition above?
virtid = prop_add(i.lnum, 0, {
type: get(virttype, toupper(i.type), virttype['']),
bufnr: i.bufnr,
text: prefix .. i.text->split('\n')[0]->trim(),
text_align: text_align,
text_padding_left: align == 'below' || text_align == 'above' ? indent(i.lnum) : 2,
})
# Save the text-property IDs for later removal
add(ids[i.bufnr], virtid)
endfor
# TODO check if there's an error when adding text-properties when user runs
# something like :cdo! global/\%#/delete_
# Also check if an error occurs when we manually delete some of the lines in
# quickfix list with bufload(mybuf) and deletebufline(mybuf, lnum)
autocmd_add([
{
group: 'qf-text-props',
event: 'BufWinEnter',
pattern: '*',
replace: true,
cmd: "On_bufwinenter()"
},
{
group: 'qf-text-props',
event: 'BufReadPost',
replace: true,
pattern: '*',
cmd: "On_bufread()"
}
])
if loclist
autocmd_add([{
group: 'qf-text-props',
event: 'WinClosed',
pattern: string(group),
replace: true,
once: true,
cmd: "On_winclosed()"
}])
endif
enddef
def Virtualtext_remove(loclist: bool, bang: bool = false)
if loclist
if bang
for i in keys(qfs)
if str2nr(i) != 0
Props_remove(str2nr(i))
endif
endfor
autocmd_delete([{group: 'qf-text-props', event: 'WinClosed'}])
else
const group: number = Group_id(loclist)
Props_remove(group)
autocmd_delete([{
group: 'qf-text-props',
event: 'WinClosed',
pattern: string(group)
}])
endif
else
Props_remove(0)
endif
enddef
def Virtualtext_toggle(loclist: bool, align: string)
if has_key(qfs, Group_id(loclist))
Virtualtext_remove(loclist)
else
Virtualtext_add(loclist, align)
endif
enddef
def Complete(arglead: string, cmdline: string, curpos: number): string
return join(['after', 'right', 'below', 'above'], "\n")
enddef
command -nargs=? -complete=custom,Complete VirtualText Virtualtext_add(false, <q-args>)
command -nargs=? -complete=custom,Complete LVirtualText Virtualtext_add(true, <q-args>)
command -nargs=? -complete=custom,Complete VirtualTextToggle Virtualtext_toggle(false, <q-args>)
command -nargs=? -complete=custom,Complete LVirtualTextToggle Virtualtext_toggle(true, <q-args>)
command VirtualTextClear Virtualtext_remove(false)
command -bang LVirtualTextClear Virtualtext_remove(true, <bang>0)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment