---Quickfixの中身を表として出力する --- Synopsis --- :QfTable --- :QfTable -format=markdown -column=bufname,lnum,text -output=reg:* --- Arguments --- -format 出力フォーマットを指定する。markdownのみ対応。 default: markdown --- -column 各列に出力する要素をカンマ(,)つなぎで指定する。 default: bufname,lnum,text --- 指定出来る要素: name, bufname, module, lnum, end_lnum, col, end_col, vcol, nr, pattern, text, type, valid --- -output 出力先を指定する。 default: hnew --- 出力先には以下の4種類を指定出来る。 --- - cur カーソル行の下に追記する --- - hnew[:name] 横分割で新しいバッファ・ウインドウを開いて記入する。 :name のようにコロンに続けてバッファ名を指定出来る。 --- - vnew[:name] 縦分割で新しいバッファ・ウインドウを開いて記入する。 :name のようにコロンに続けてバッファ名を指定出来る。 --- - reg[:name] レジスタ |registers| に書き込む。`:a`や`:A`のようにコロンに続けてレジスタ名を指定できる。省略した場合はunnamed registerに書き込む。 -- Columnの表現 ---@param n number|nil local function column_itoa(n) if n == nil then return "" end return string.format("%d", n) end ---@param b boolean|nil local function column_btoa(b) if b == nil then return "" end if b then return "yes" end return "" end ---@param s string|nil local function column_raw(s) if s == nil then return "" end return s end ---@class QuickfixColumn ---@field source string Column source ---@field postprocess fun(n: any): string ---@field title string Title ---@field length? number A length to display ---@type table local VALID_COLUMNS = { bufnr = { --number of buffer source = "bufnr", postprocess = column_itoa, title = "Buffer", }, bufname = { -- name of the buffer (file name) source = "bufnr", postprocess = function(nr) return vim.fn.bufname(nr) end, title = "Name", }, module = { --module name source = "module", postprocess = column_raw, title = "Module", }, lnum = { --line number in the buffer (first line is 1) source = "lnum", postprocess = column_itoa, title = "Line", }, end_lnum = { --end of line number if the item is multiline source = "end_lnum", postprocess = column_itoa, title = "End Line", }, col = { --column number (first column is 1) source = "col", postprocess = column_itoa, title = "Column", }, end_col = { --end of column number if the item has range source = "end_col", postprocess = column_itoa, title = "End Column", }, vcol = { --|TRUE|: "col" is visual column:|FALSE|: "col" is byte index source = "vcol", postprocess = column_btoa, title = "Visual Column", }, nr = { --error number source = "nr", postprocess = column_itoa, title = "Error Number", }, pattern = { --search pattern used to locate the error source = "pattern", postprocess = column_raw, title = "Match Pattern", }, text = { --description of the error source = "text", postprocess = column_raw, title = "Text", }, type = { --type of the error, 'E', '1', etc. source = "type", postprocess = column_raw, title = "Error Type", }, valid = { --|TRUE|: recognized error message source = "valid", postprocess = column_btoa, title = "Valid Error", }, } ---@type string[] local DEFAULT_COLUMN_NAMES = { "bufname", "lnum", "text" } -- フォーマット --- Markdown形式でフォーマットする ---@param printer Printer ---@param columns QuickfixColumn[] ---@param rows string[][])> local function format_markdown(printer, columns, rows) printer:put("| " .. table.concat( vim.tbl_map(function(col) return col.title .. string.rep(" ", col.length - #col.title) end, columns), " | " ) .. " |") printer:put("| " .. table.concat( vim.tbl_map(function(col) return "-" .. string.rep(" ", col.length - 1) end, columns), " | " ) .. " |") for _, row in ipairs(rows) do local cells = {} for c, cell in ipairs(row) do local col = columns[c] table.insert(cells, cell .. string.rep(" ", col.length - #cell)) end printer:put("| " .. table.concat(cells, " | ") .. " |") end end ---@type table local VALID_FORMATS = { markdown = format_markdown, } local DEFAULT_FORMAT = "markdown" --- 出力先 ---@class Printer local Printer = {} ---@param target string Output target function Printer:open(target) error("not implemented: " .. target) end ---@param text string Output value function Printer:put(text) error("not implemented: " .. text) end ---@class CurPrinter : Printer local CurPrinter = {} ---@return CurPrinter function CurPrinter.new() return setmetatable({ line = vim.fn.line(".") }, { __index = CurPrinter }) end function CurPrinter:open(_) end ---@param text string Output value function CurPrinter:put(text) vim.fn.append(self.line, text) self.line = self.line + 1 end ---@class BufPrinter : Printer ---@field cmd fun(target?: string) A command to open new buffer window local BufPrinter = {} ---@return BufPrinter function BufPrinter.hnew() return setmetatable({ line = 1, cmd = vim.cmd.new }, { __index = BufPrinter }) end ---@return BufPrinter function BufPrinter.vnew() return setmetatable({ line = 1, cmd = vim.cmd.vnew }, { __index = BufPrinter }) end ---@param target string Output target function BufPrinter:open(target) if target == "" then self.cmd() else self.cmd(target) end end ---@param text string Output value function BufPrinter:put(text) vim.fn.setline(self.line, text) self.line = self.line + 1 end ---@class RegPrinter : Printer local RegPrinter = {} ---@return RegPrinter function RegPrinter.new() return setmetatable({ line = 1, option = "l" }, { __index = RegPrinter }) end ---@type object local valid_reg = vim.regex([[^[a-zA-Z0-9\*+]$]]) ---@param target string Output target function RegPrinter:open(target) if target ~= "" and not valid_reg:match_str(target) then error(string.format("invalid argument: %q is not valid register name", target)) end self.regname = target end ---@param text string Output value function RegPrinter:put(text) vim.fn.setreg(self.regname, text, self.option) self.option = self.option .. "a" end ---@type table local VALID_OUTPUTS = { cur = CurPrinter.new, -- vim.fn.append() hnew = BufPrinter.hnew, -- vim.cmd.new, vim.fn.setline() vnew = BufPrinter.vnew, -- vim.cmd.vnew, vim.fn.setline() reg = RegPrinter.new, -- vim.fn.setreg("?", line, "la"), } local DEFAULT_OUTPUT = BufPrinter.hnew -- 処理本体 local function quickfix_to_table(event) local format = DEFAULT_FORMAT local columnNames = DEFAULT_COLUMN_NAMES local printerFactory = DEFAULT_OUTPUT local name = "" for _, arg in pairs(event.fargs) do if vim.startswith(arg, "-format=") then format = string.sub(arg, 9) elseif vim.startswith(arg, "-column=") then columnNames = vim.split(string.sub(arg, 9), ",", { trimempty = true, plain = true }) elseif vim.startswith(arg, "-output=") then local terms = vim.split(string.sub(arg, 9), ":", { trimempty = true, plain = true }) printerFactory = VALID_OUTPUTS[terms[1]] if not printerFactory then error(string.format("invalid argument: %q is not valid printer name")) end if #terms >= 2 then name = terms[2] end end end ---@type QuickfixColumn[] local columns = vim.tbl_map(function(c) local column = VALID_COLUMNS[c] if not column then error(string.format("invalid argument: %q is not valid column name", c)) end return vim.tbl_deep_extend("force", { length = #column.title }, column) end, columnNames) local formatter = VALID_FORMATS[format] if not formatter then error(string.format("invalid argument: %q is no valid format", format)) end ---@type string[][] local rows = {} for _, item in ipairs(vim.fn.getqflist()) do ---@type string[] local cells = {} for c, column in ipairs(columns) do local v = column.postprocess(vim.tbl_get(item, column.source)) columns[c].length = math.max(columns[c].length, #v) table.insert(cells, v) end table.insert(rows, cells) end local printer = printerFactory() printer:open(name) formatter(printer, columns, rows) end -- コマンド/keymap設定 vim.api.nvim_create_user_command("QfTable", quickfix_to_table, { force = true, range = true, nargs = "*" }) vim.cmd([[ cabbrev Qftable (getcmdtype() ==# ":" && getcmdline() ==# "Qftable") ? "QfTable" : "Qftable" ]]) vim.keymap.set({ "n", "v" }, "qt", "QfTable", { remap = false })