1 -- SPDX-License-Identifier: GPL-3.0-or-later
2 -- © 2020 Georgi Kirilov
9 if vis
:module_exist
"lfs" then
13 getcwd
= lfs
.currentdir
16 return io
.popen
"pwd":read"*l"
24 "^%s*([^:]+):(%d+):(%d+):(.*)$", -- git-grep with --column
25 "^%s*([^:]+):(%d+):(.-)(.*)$",
28 "^%s*([^:]+):(%d+):(%d+):(.*)$",
29 "^%s*([^:]+):(%d+):(.-)(.*)$",
30 "^(%S+) %S+ (%d+) (.-)(.*)$", -- cscope
32 ["Entering directory [`']([^']+)'"] = true,
33 ["Leaving directory [`']([^']+)'"] = false,
38 errorfile
= "errors.err",
46 local lines
= {valid
= {}}
47 local no_more
= "No more items"
48 local no_errors
= "No Errors"
50 local function find_nearest_after(line
)
52 for c
, v
in ipairs(lines
.valid
) do
61 return {ccur
, lines
.valid
[ccur
][1]}
64 local function find_nth_valid(count
)
65 if count
< 1 or count
> #lines
.valid
then
68 return {count
, lines
.valid
[count
][1]}
71 local function set_marks(win
, ccur
)
72 if not ccur
then return end
73 local fname
= win
.file
.name
75 local pathname
= fname
:find("^/") and fname
or pwd
.. "/" .. fname
77 while lines
.valid
[i
] and lines
.valid
[i
].path
== pathname
do
80 local ln
= lines
.valid
[i
+ 1]
81 while ln
and ln
.path
== pathname
do
82 -- I wish there was a way to convert from ln:col to pos
83 -- without setting the selection
84 win
.selection
:to(ln
.line
, ln
.column
or 1)
85 ln
.mark
= win
.file
:mark_set(win
.selection
.pos
)
91 local function botright_reopen(filename
, ccur
)
92 -- This function closes and opens windows in a specific order, just so
93 -- the error window ends up at bottom position.
94 -- This is fragile, and does not work in all possible situations.
95 -- Having a window with a modified file is one example.
96 -- Even if the file was not modified, closing the window will lead to loss of
97 -- any state local to it.
98 -- It would be nice if vis had :botright or something to that effect.
100 for w
in vis
:windows() do
102 cursors
[w
.file
.name
] = w
.selection
.pos
104 if cwin
and w
~= cwin
then
108 for w
in vis
:windows() do
109 if w
.file
.name
and w
.file
.modified
then
110 vis
:info
"No write since last change"
115 vis
:command(string.format((cwin
and "open" or "e") .. " %q", filename
))
116 set_marks(vis
.win
, ccur
)
117 if cursors
[filename
] then
118 vis
.win
.selection
.pos
= cursors
[filename
]
126 local function counter(ccur
)
127 return string.format("%s/%d",
132 local function display(ccur
, cline
)
133 local ln
= lines
.valid
[ccur
]
135 local cname
= ln
.path
:find(pwd
) and ln
.path
:gsub(pwd
, "", 1):gsub("^/", "") or ln
.path
136 if vis
.win
.file
.name
~= cname
then
137 if not botright_reopen(ln
.path
, ccur
) then return end
139 local column
= ln
.column
140 if type(column
) ~= "number" then
143 -- TODO: some tools report virtual columns, others - physical.
144 -- local indent = vis.win.file.lines[ln.line]:match"^%s+" or ""
145 -- local _, tabs = indent:gsub("\t", "")
146 -- -- XXX: assume tools to report 8 columns-wide tabs
147 -- column = column - tabs * 8 + tabs
150 cwin
.selection
:to(cline
, 1)
151 local pos
= cwin
.selection
.pos
152 local clen
= cwin
.file
.lines
[cline
]
153 lines
.range
= {start
= pos
, finish
= pos
+ #clen
- 1}
156 local newpos
= vis
.win
.file
:mark_get(ln
.mark
)
158 vis
.win
.selection
.pos
= newpos
161 -- XXX: degrade to using raw line:column;
162 -- so far, only triggered by consecutive hard links
163 -- where vis keeps the old file.name but the error list
164 -- switches to the new. set_marks gets confused and sets no marks.
165 vis
.win
.selection
:to(ln
.line
, column
or 1)
167 if not cwin
and ln
.message
then
168 vis
:info(string.format("[%s] %s", counter(ccur
), ln
.message
:gsub("^%s", "")))
174 local function _cc(count
)
175 if not count
then return end
176 local location
, err
= find_nth_valid(count
)
181 return table.unpack(location
)
184 local function guard(func
)
186 if #lines
.valid
== 0 then
190 local ccur
, cline
= func(...)
191 if ccur
and cline
then
197 local cc
= guard(function(count
)
198 return _cc(count
or lines
.ccur
or 1)
201 local cnext
= guard(function(count
)
202 return _cc((lines
.ccur
or 0) + (count
or 1))
205 local cprev
= guard(function(count
)
206 return _cc((lines
.ccur
or 2) - (count
or 1))
209 local crewind
= guard(function()
213 local clast
= guard(function()
214 return _cc(#lines
.valid
)
217 local cnfile
= guard(function(count
)
219 if not lines
.ccur
then
222 local cur_fname
= lines
.valid
[lines
.ccur
].path
223 for i
= lines
.ccur
+ 1, #lines
.valid
do
224 local filename
= lines
.valid
[i
].path
226 if filename
~= cur_fname
then
230 return i
, lines
.valid
[i
][1]
237 local cpfile
= guard(function(count
)
239 if not lines
.ccur
then
242 local cur_fname
= lines
.valid
[lines
.ccur
].path
243 for i
= lines
.ccur
- 1, 1, -1 do
244 local filename
= lines
.valid
[i
].path
246 if filename
~= cur_fname
then
250 return i
, lines
.valid
[i
][1]
257 local function open_error_window()
258 if cwin
then return end
263 local fname
= vis
.win
.file
.name
266 cwin
.file
:insert(0, lines
.buffer
or "")
267 cwin
.file
.modified
= false
272 local first
= find_nth_valid(1)
273 cline1
= first
and first
[2] or 1
275 cwin
.selection
:to(cline1
, 1)
277 local pos
= cwin
.selection
.pos
278 local clen
= cwin
.file
.lines
[lines
.cline
]
279 lines
.range
= {start
= pos
, finish
= pos
+ #clen
- 1}
281 if #lines
.valid
> 0 then
283 cwin
.options
.cursorline
= true
285 vis
:command
"set cursorline"
288 cwin
:map(vis
.modes
.NORMAL
, "<Enter>", function()
289 if #lines
.valid
== 0 then
293 local location
, err
= find_nearest_after(vis
.win
.selection
.line
)
298 display(table.unpack(location
))
303 botright_reopen(fname
, lines
.ccur
)
304 vis
:feedkeys
"<vis-window-prev>"
307 local function cwindow()
315 local function store_from_string(str
, fmt
)
316 if str
and string.len(str
) == 0 then
319 lines
= {buffer
= str
, valid
= {}}
320 if not lines
.buffer
then return end
323 elseif type(fmt
) == "string" then
329 for ln
in lines
.buffer
:gmatch("[^\n]*") do
331 for patt
, push
in pairs(fmt
[0] or {}) do
332 local dir
= ln
:match(patt
)
335 table.insert(dirstack
, dir
)
336 elseif dirstack
[#dirstack
] == dir
then
337 table.remove(dirstack
)
341 local cwd
= dirstack
[#dirstack
] or pwd
342 local filename
, line
, column
, message
343 for _
, f
in ipairs(fmt
) do
344 filename
, line
, column
, message
= ln
:match(f
)
345 if filename
and line
then
349 if filename
and line
then
350 local pathname
= filename
:find("^/") and filename
or string.format("%s/%s", cwd
, filename
)
351 local t
= {i
, path
= pathname
, line
= tonumber(line
), column
= tonumber(column
), message
= message
}
352 table.insert(lines
.valid
, t
)
357 local function store_from_file(errorfile
)
359 M
.errorfile
= errorfile
361 local efile
= io
.open(errorfile
or M
.errorfile
)
363 vis
:info(string.format("Can't open errorfile %s", errorfile
or M
.errorfile
))
366 local str
= efile
:read"*all"
368 store_from_string(str
)
372 local function store_from_window(win
)
373 local str
= win
.file
:content(0, win
.file
.size
)
374 store_from_string(str
)
377 local function cfile(argv
)
378 if store_from_file(argv
[1]) then
384 ctitle
= string.format(argv
[1] and "%s %s" or "%s", argv
[0], argv
[1])
392 local function cbuffer(argv
)
393 store_from_window(vis
.win
)
394 local fname
= vis
.win
.file
.name
395 ctitle
= string.format(fname
and "%s (%s)" or "%s", argv
[0], fname
)
396 vis
.win
.file
.modified
= false
400 local function _cexpr(cmd
, fmt
, title
, is_make
)
401 if not cmd
or #cmd
== 0 then vis
:info
"Argument required" return end
402 ctitle
= title
or cmd
403 local code
, stdout
= vis
:pipe(nil, nil, cmd
.. " 2>&1")
409 store_from_string(stdout
, fmt
)
410 lines
.code
= code
~= 0 and code
or nil
411 if is_make
and code
== 0 then
412 vis
:info(string.format("'%s' finished", M
.makeprg
))
415 if was_open
or M
.peek
or #lines
.valid
== 0 then
418 if not M
.peek
and #lines
.valid
> 0 then
423 local function quote_spaces(argv
)
424 for i
, arg
in ipairs(argv
) do
425 if arg
:find("[ \t\n]") then
426 argv
[i
] = "'" .. arg
.. "'"
431 local function cexpr(argv
)
433 _cexpr(table.concat(argv
, " "))
436 local function grep(argv
)
438 table.insert(argv
, 1, M
.grepprg
)
439 _cexpr(table.concat(argv
, " "), M
.grepformat
)
442 local function make(argv
)
444 table.insert(argv
, 1, M
.makeprg
)
445 _cexpr(table.concat(argv
, " "), M
.errorformat
, nil, true)
448 local function h(msg
)
449 return string.format("|@%s| %s", progname
, msg
)
452 vis
.events
.subscribe(vis
.events
.INIT
, function()
453 -- These commands assume an existing error list:
455 cn
= {cnext
, "Display the [arg]-th next error"},
456 cp
= {cprev
, "Display the [arg]-th previous error"},
457 cnf
= {cnfile
, "Display the first error in the [arg]-th next file"},
458 cpf
= {cpfile
, "Display the last error in the [arg]-th previous file"},
459 cc
= {cc
, "Display [arg]-th error. If [arg] is omitted, the same error is displayed again."},
460 cr
= {crewind
, "Display the first error"},
461 cla
= {clast
, "Display the last error"},
463 -- These commands create a new error list:
465 cf
= {cfile
, "Read the error list from [arg]"},
466 cb
= {cbuffer
, "Read the error list from the current file"},
467 cex
= {cexpr
, "Create an error list using the result of [args]"},
468 grep
= {grep
, string.format("Create an error list using the result of '%s'", M
.grepprg
)},
469 make
= {make
, string.format("Create an error list using the result of '%s'", M
.makeprg
)},
470 cw
= {cwindow
, "Toggle the error window"},
472 for cmd
, def
in pairs(ccommands
) do
473 local func
, help
= table.unpack(def
)
474 vis
:command_register(cmd
, function(argv
)
475 local count
= argv
[1] and tonumber(argv
[1])
478 M
.action
[cmd
] = function(arg
)
479 -- XXX: do not convert, say, "1" to 1; a digit can be passed by vis.map but it is not a count
480 local count
= type(arg
) == "number" and arg
484 for cmd
, def
in pairs(qcommands
) do
485 local func
, help
= table.unpack(def
)
486 vis
:command_register(cmd
, func
, h(help
))
489 vis
:option_register("qfm", "bool", function(value
, toggle
)
495 end, h
"Menu - jumping to an error with <Enter> closes the error window")
496 vis
:option_register("qfp", "bool", function(value
, toggle
)
502 end, h
"Peek - :make, :grep, and :cex do not jump to the first error")
505 vis
.events
.subscribe(vis
.events
.WIN_STATUS
, function(win
)
506 if win
~= cwin
then return end
508 string.format(" [Quickfix List]%s :%s", (win
.file
.modified
and " [+]" or ""), ctitle
),
509 lines
.code
and string.format("exit: %d « [%s] ", lines
.code
, counter(lines
.ccur
))
510 or string.format("[%s] ", counter(lines
.ccur
))
514 vis
.events
.subscribe(vis
.events
.WIN_CLOSE
, function(win
)
515 if win
~= cwin
then return end
519 vis
.events
.subscribe(vis
.events
.WIN_HIGHLIGHT
, function(win
)
520 if win
~= cwin
then return end
521 if not (lines
and lines
.range
) then return end
522 win
:style(win
.STYLE_CURSOR_PRIMARY
, lines
.range
.start
, lines
.range
.finish
)