|
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) |