-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathpanel.lua
More file actions
1118 lines (1003 loc) · 33 KB
/
panel.lua
File metadata and controls
1118 lines (1003 loc) · 33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
local util = require "codeql.util"
local config = require "codeql.config"
local Popup = require "nui.popup"
local autocmd = require "nui.utils.autocmd"
local event = autocmd.event
local vim = vim
local range_ns = vim.api.nvim_create_namespace "codeql"
local panel_short_help = true
local icon_closed = "▶"
local icon_open = "▼"
-- Render process:
-- render()
-- render_content()
-- print_help()
-- print_header()
-- print_issues()
-- print_tree_nodes()
-- print_tree_node()
-- global variables
local M = {
panels = {},
ns = range_ns,
}
_G.generate_issue_label = function(node)
local label = node.label
local conf = config.values
if conf.panel.show_filename and node["filename"] and node["filename"] ~= nil then
if conf.panel.long_filename then
label = node.filename
elseif #vim.fn.fnamemodify(node.filename, ":p:t") > 0 then
label = vim.fn.fnamemodify(node.filename, ":p:t")
else
label = node.label
end
if node.line and node.line > 0 then
label = label .. ":" .. node.line
end
end
return label
end
local function register(bufnr, obj)
local curline = vim.api.nvim_buf_line_count(bufnr)
M.panels[bufnr].line_map[curline] = obj
end
local function flatten_label(text)
return table.concat(vim.split(tostring(text), "\n"), " \\n ")
end
local function print_to_panel(bufnr, text, matches)
text = flatten_label(text)
vim.api.nvim_buf_set_lines(bufnr, -1, -1, true, { text })
if type(matches) == "table" then
for hlgroup, groups in pairs(matches) do
for _, group in ipairs(groups) do
local linenr = vim.api.nvim_buf_line_count(bufnr) - 1
vim.api.nvim_buf_add_highlight(bufnr, 0, hlgroup, linenr, group[1], group[2])
end
end
end
end
local function get_panel_window(bufnr)
for _, w in ipairs(vim.api.nvim_list_wins()) do
if vim.api.nvim_win_get_buf(w) == bufnr then
return w
end
end
return nil
end
local function go_to_main_window(panel_name)
-- go to the wider window
local widerwin = 0
local widerwidth = 0
local current_tab = vim.api.nvim_get_current_tabpage()
local tab_windows = vim.api.nvim_tabpage_list_wins(current_tab)
for _, w in ipairs(tab_windows) do
if vim.api.nvim_win_get_width(w) > widerwidth then
if vim.api.nvim_win_get_buf(w) ~= vim.fn.bufnr(panel_name) then
widerwin = w
widerwidth = vim.api.nvim_win_get_width(w)
end
end
end
if widerwin > -1 then
vim.fn.win_gotoid(widerwin)
end
end
local function is_filtered(filter, issue)
local f, err = loadstring("return function(issue) return " .. filter .. " end")
if f then
return f()(issue)
else
return f, err
end
end
local function print_help(bufnr)
util.debug "Entering panel.print_help()"
if panel_short_help then
print_to_panel(bufnr, '" Press H for help')
print_to_panel(bufnr, "")
else
print_to_panel(bufnr, '" --------- General ---------')
print_to_panel(bufnr, '" <CR>: Jump to tag definition')
print_to_panel(bufnr, '" p: As above, but dont change window')
print_to_panel(bufnr, '" P: Previous path')
print_to_panel(bufnr, '" N: Next path')
print_to_panel(bufnr, '"')
print_to_panel(bufnr, '" --------- Filters ---------')
print_to_panel(bufnr, '" f: Label filter')
print_to_panel(bufnr, '" F: Generic filter')
print_to_panel(bufnr, '" x: Clear filter')
print_to_panel(bufnr, '" s: Shortest path filter')
print_to_panel(bufnr, '"')
print_to_panel(bufnr, '" ---------- Folds ----------')
print_to_panel(bufnr, '" o: Toggle fold')
print_to_panel(bufnr, '" O: Open all folds')
print_to_panel(bufnr, '" C: Close all folds')
print_to_panel(bufnr, '"')
print_to_panel(bufnr, '" ---------- Misc -----------')
print_to_panel(bufnr, '" m: Toggle mode')
print_to_panel(bufnr, '" q: Close window')
print_to_panel(bufnr, '" H: Toggle help')
print_to_panel(bufnr, "")
end
end
local function get_node_location(node)
if node.line and node.filename then
local line = ""
if node.line > -1 then
line = string.format(":%d", node.line)
end
local filename
local conf = config.values
if conf.panel.long_filename then
filename = node.filename
else
filename = vim.fn.fnamemodify(node.filename, ":p:t")
end
return filename .. line
else
return ""
end
end
local function print_tree_node(bufnr, node, indent_level)
local start_time = util.debug "Entering panel.print_tree_node()"
local text = ""
local hl = {}
-- mark
local mark = string.rep(" ", indent_level) .. node.mark .. " "
local mark_hl_name = ""
if node.mark == "≔" then
mark_hl_name = "CodeqlPanelLabel"
else
mark_hl_name = node.visitable and "CodeqlPanelVisitable" or "CodeqlPanelNonVisitable"
end
hl[mark_hl_name] = { { 0, string.len(mark) } }
-- text
if node.filename then
local location = get_node_location(node)
text = string.format("%s%s - %s", mark, location, node.label)
local sep_index = string.find(text, " - ", 1, true)
hl["CodeqlPanelFile"] = { { string.len(mark), sep_index } }
hl["CodeqlPanelSeparator"] = { { sep_index, sep_index + 2 } }
else
text = mark .. "[" .. node.label .. "]"
end
print_to_panel(bufnr, text, hl)
register(bufnr, {
kind = "node",
obj = node,
})
util.debug("Exiting panel.print_tree_node()", { start_time = start_time })
end
local function left_align(text, size)
return text .. string.rep(" ", size - vim.fn.strdisplaywidth(text))
end
local function right_align(text, size)
return string.rep(" ", size - vim.fn.strdisplaywidth(text)) .. text
end
local function center_align(text, size)
local pad = size - vim.fn.strdisplaywidth(text)
local left_pad = math.floor(pad / 2)
local right_pad = pad - left_pad
return string.rep(" ", left_pad) .. text .. string.rep(" ", right_pad)
end
local function align(text, size)
local alignment = config.values.panel.alignment
if alignment == "left" then
return left_align(text, size)
elseif alignment == "right" then
return right_align(text, size)
elseif alignment == "center" then
return center_align(text, size)
else
return text
end
end
-- returns a tuple, where the first element is a list of the column labels and the second
-- element is a list of column locations
local function get_row_columns(issue, max_lengths)
local path = issue.paths[1]
local labels = {}
local locations = {}
for i, node in ipairs(path) do
table.insert(labels, align(flatten_label(node.label), max_lengths[i]))
table.insert(locations, align(get_node_location(node), max_lengths[i]))
end
return { labels, locations }
end
local function print_tree_nodes(bufnr, issue, indent_level)
local start_time = util.debug "Entering panel.print_tree_nodes()"
local line_map = M.panels[bufnr].line_map
local curline = vim.api.nvim_buf_line_count(bufnr)
local paths = issue.paths
-- paths
local active_path = 1
if #paths > 1 then
if line_map[curline] and line_map[curline].kind == "issue" then
-- retrieve path info from the line_map
active_path = line_map[curline].obj.active_path
end
local str = active_path .. "/" .. #paths
if line_map[curline + 1] then
table.remove(line_map, curline + 1)
end
local text = string.rep(" ", indent_level) .. "Path: "
local hl = { CodeqlPanelInfo = { { 0, string.len(text) } } }
print_to_panel(bufnr, text .. str, hl)
register(bufnr)
end
local path = paths[active_path]
-- print path nodes
for _, node in ipairs(path) do
print_tree_node(bufnr, node, indent_level)
end
util.debug("Existing panel.print_tree_nodes()", { start_time = start_time })
end
local function print_header(bufnr, issues)
util.debug "Entering panel.print_header()"
if config.database.path then
local hl = { CodeqlPanelInfo = { { 0, string.len "Database:" } } }
print_to_panel(bufnr, "Database: " .. config.database.path, hl)
elseif config.sarif.path then
local hl = { CodeqlPanelInfo = { { 0, string.len "SARIF:" } } }
local parts = vim.split(config.sarif.path, "/")
local filename = parts[#parts]
print_to_panel(bufnr, "SARIF: " .. filename, hl)
end
local hl = { CodeqlPanelInfo = { { 0, string.len "Issues:" } } }
-- print_to_panel(bufnr, "Issues: " .. table.getn(issues), hl)
print_to_panel(bufnr, "Issues: " .. #issues, hl)
end
local function get_column_names(columns, max_lengths)
local result = {}
for i, column in ipairs(columns) do
table.insert(result, align(column, max_lengths[i]))
end
return result
end
local function min_path_length(paths)
local min_length = #paths[1]
for _, path in ipairs(paths) do
if #path < min_length then
min_length = #path
end
end
return min_length
end
local function print_tree(bufnr, results)
util.debug "Entering print_tree()"
-- print group name
local query_foldmarker = not results.is_folded and icon_open or icon_closed
local query_label = string.format("%s %s", query_foldmarker, results.label)
print_to_panel(bufnr, string.format("%s (%d)", query_label, #results.issues), {
CodeqlPanelFoldIcon = { { 0, string.len(query_foldmarker) } },
CodeqlPanelQueryId = { { string.len(query_foldmarker), string.len(query_label) } },
})
register(bufnr, {
kind = "query",
obj = results,
})
if not results.is_folded then
-- print issue labels
util.debug "Printing issue labels"
for _, issue in ipairs(results.issues) do
-- print nodes
if not issue.hidden then
local start_rtime = util.debug "Printing issue label"
local is_folded = issue.is_folded
local foldmarker = not is_folded and icon_open or icon_closed
local label = string.format(" %s %s ↔ %d", foldmarker, issue.label, issue.min_path_length)
print_to_panel(bufnr, label, {
CodeqlPanelFoldIcon = { { 0, 2 + string.len(foldmarker) } },
Normal = { { 2 + string.len(foldmarker), 2 + string.len(foldmarker) + string.len(issue.label) } },
CodeqlPanelQueryId = {
{
4 + string.len(foldmarker) + string.len(issue.label),
4 + string.len(foldmarker) + string.len(issue.label) + 2,
},
},
})
register(bufnr, {
kind = "issue",
obj = issue,
})
util.debug("Finished printing issue label", { start_time = start_rtime })
if not is_folded then
print_tree_nodes(bufnr, issue, 4)
end
end
end
end
print_to_panel(bufnr, "")
end
local function print_table(bufnr, results)
-- TODO: node.label may need to be tweaked (eg: replace new lines with "")
-- and this is the place to do it
util.debug "Entering print_table()"
print_to_panel(bufnr, string.format("%s (%d)", results.label, #results.issues), {
CodeqlPanelQueryId = { { 0, string.len(results.label) } },
})
-- calculate max length for each cell
local max_lengths = {}
for _, issue in ipairs(results.issues) do
-- in table view, we only show the first path
local path = issue.paths[1]
-- in table view, each path node is a column
for i, column in ipairs(path) do
-- we need to take into account the length of the label and the location
-- the cell length is the max of the label and the location
max_lengths[i] = math.max(vim.fn.strdisplaywidth(flatten_label(column.label)), max_lengths[i] or -1)
max_lengths[i] = math.max(vim.fn.strdisplaywidth(get_node_location(column)), max_lengths[i] or -1)
end
end
if results.columns and #results.columns > 0 then
for i, column in ipairs(results.columns) do
max_lengths[i] = math.max(vim.fn.strdisplaywidth(column), max_lengths[i] or -1)
end
end
--local total_length = 4
--for _, len in ipairs(max_lengths) do
-- total_length = total_length + len + 3
--end
print_to_panel(bufnr, "")
local rows = {}
for _, issue in ipairs(results.issues) do
local row_columns = get_row_columns(issue, max_lengths)
table.insert(rows, row_columns)
end
local bars = {}
for _, len in ipairs(max_lengths) do
table.insert(bars, string.rep("─", len))
end
local column_names = get_column_names(results.columns, max_lengths)
-- header
local header1 = string.format("┌─%s─┐", table.concat(bars, "─┬─"))
print_to_panel(bufnr, header1, { CodeqlPanelSeparator = { { 0, -1 } } })
if results.columns and #results.columns > 0 then
local header2 = string.format("│ %s │", table.concat(column_names, " │ "))
print_to_panel(bufnr, header2, { CodeqlPanelSeparator = { { 0, -1 } } })
local header3 = string.format("├─%s─┤", table.concat(bars, "─┼─"))
print_to_panel(bufnr, header3, { CodeqlPanelSeparator = { { 0, -1 } } })
end
local separator_hls = { { 0, vim.fn.len "│ " } }
local acc = vim.fn.len "│ "
for _, len in ipairs(max_lengths) do
table.insert(separator_hls, { acc + len, acc + len + vim.fn.len " │ " })
acc = acc + len + vim.fn.len " │ "
end
local location_hls = {}
acc = vim.fn.len "│ "
for _, len in ipairs(max_lengths) do
table.insert(location_hls, { acc, acc + len })
acc = acc + len + vim.fn.len " │ "
end
local hl_labels = { CodeqlPanelSeparator = separator_hls }
local hl_locations = { CodeqlPanelSeparator = separator_hls, Comment = location_hls }
for i, row in ipairs(rows) do
-- labels
local r = string.format("│ %s │", table.concat(row[1], " │ "))
print_to_panel(bufnr, r, hl_labels)
register(bufnr, {
kind = "row",
obj = {
ranges = location_hls,
columns = results.issues[i].paths[1],
},
})
-- locations
r = string.format("│ %s │", table.concat(row[2], " │ "))
print_to_panel(bufnr, r, hl_locations)
register(bufnr, {
kind = "row",
obj = {
ranges = location_hls,
columns = results.issues[i].paths[1],
},
})
if i < #rows then
-- row separator
r = string.format("├─%s─┤", table.concat(bars, "─┼─"))
else
-- footer
r = string.format("└─%s─┘", table.concat(bars, "─┴─"))
end
print_to_panel(bufnr, r, { CodeqlPanelSeparator = { { 0, -1 } } })
end
print_to_panel(bufnr, "")
end
local function print_issues(bufnr, results)
local start_time = util.debug "Entering panel.print_issues()"
if results.mode == "tree" then
start_time = util.debug "Entering panel.print_issues():tree mode"
print_tree(bufnr, results)
elseif results.mode == "table" then
start_time = util.debug "Entering panel.print_issues():table mode"
print_table(bufnr, results)
end
util.debug("Exiting panel.print_issues()", { start_time = start_time })
end
local function render_content(bufnr)
local start_time = util.debug "Entering panel.render_content()"
if not bufnr or bufnr == -1 then
util.err_message "Error opening CodeQL panel"
return
end
local panel = M.panels[bufnr]
vim.api.nvim_buf_set_option(bufnr, "modifiable", true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {})
print_help(bufnr)
if #panel.issues > 0 then
print_header(bufnr, panel.issues)
print_to_panel(bufnr, "")
for _, query in ipairs(panel.queries) do
print_issues(bufnr, query)
end
local win = get_panel_window(bufnr)
local lcount = vim.api.nvim_buf_line_count(bufnr)
vim.api.nvim_win_set_cursor(win, { math.min(7, lcount), 0 })
else
print_to_panel(bufnr, "No results found.")
end
vim.api.nvim_buf_set_option(bufnr, "modifiable", false)
util.debug("Existing panel.render_content()", { start_time = start_time })
end
local function render_keep_view(bufnr, line)
local start_time = util.debug "Entering panel.render_keep_view()"
if line == nil then
line = vim.fn.line "."
end
-- called from toggle_fold commands, so within panel buffer
local curcol = vim.fn.col "."
local topline = vim.fn.line "w0"
render_content(bufnr)
local scrolloff_save = vim.api.nvim_get_option "scrolloff"
vim.cmd "set scrolloff=0"
vim.fn.cursor(topline, 1)
vim.cmd "normal! zt"
vim.fn.cursor(line, curcol)
local start_rtime = util.debug "Redrawing the screen"
vim.cmd("let &scrolloff = " .. scrolloff_save)
vim.cmd "redraw"
util.debug("Finishing redrawing the screen", { start_time = start_rtime })
util.debug("Exiting panel.render_keep_view()", { start_time = start_time })
end
-- exported functions
function M.apply_mappings()
local bufnr = vim.api.nvim_get_current_buf()
vim.api.nvim_buf_set_keymap(
bufnr,
"n",
"o",
[[<cmd>lua require'codeql.panel'.toggle_fold()<CR>]],
{ script = true, silent = true }
)
vim.api.nvim_buf_set_keymap(
bufnr,
"n",
"m",
[[<cmd>lua require'codeql.panel'.toggle_mode()<CR>]],
{ script = true, silent = true }
)
vim.api.nvim_buf_set_keymap(
bufnr,
"n",
"<CR>",
[[<cmd>lua require'codeql.panel'.jump_to_code(false)<CR>]],
{ script = true, silent = true }
)
vim.api.nvim_buf_set_keymap(
bufnr,
"n",
"p",
[[<cmd>lua require'codeql.panel'.jump_to_code(true)<CR>]],
{ script = true, silent = true }
)
vim.api.nvim_buf_set_keymap(
bufnr,
"n",
"s",
[[<cmd>lua require'codeql.panel'.preview_snippet(true)<CR>]],
{ script = true, silent = true }
)
vim.api.nvim_buf_set_keymap(
bufnr,
"n",
"<S-h>",
[[<cmd>lua require'codeql.panel'.toggle_help()<CR>]],
{ script = true, silent = true }
)
vim.api.nvim_buf_set_keymap(
bufnr,
"n",
"q",
[[<cmd>lua require'codeql.panel'.close_panel()<CR>]],
{ script = true, silent = true }
)
vim.api.nvim_buf_set_keymap(
bufnr,
"n",
"<S-o>",
[[<cmd>lua require'codeql.panel'.set_fold_level(false)<CR>]],
{ script = true, silent = true }
)
vim.api.nvim_buf_set_keymap(
bufnr,
"n",
"<S-c>",
[[<cmd>lua require'codeql.panel'.set_fold_level(true)<CR>]],
{ script = true, silent = true }
)
vim.api.nvim_buf_set_keymap(
bufnr,
"n",
"<S-p>",
[[<cmd>lua require'codeql.panel'.change_path(-1)<CR>]],
{ script = true, silent = true }
)
vim.api.nvim_buf_set_keymap(
bufnr,
"n",
"N",
[[<cmd>lua require'codeql.panel'.change_path(1)<CR>]],
{ script = true, silent = true }
)
vim.api.nvim_buf_set_keymap(
bufnr,
"n",
"f",
[[<cmd>lua require'codeql.panel'.label_filter()<CR>]],
{ script = true, silent = true }
)
vim.api.nvim_buf_set_keymap(
bufnr,
"n",
"<S-f>",
[[<cmd>lua require'codeql.panel'.generic_filter()<CR>]],
{ script = true, silent = true }
)
vim.api.nvim_buf_set_keymap(
bufnr,
"n",
"x",
[[<cmd>lua require'codeql.panel'.clear_filter()<CR>]],
{ script = true, silent = true }
)
vim.api.nvim_buf_set_keymap(
bufnr,
"n",
"s",
[[<cmd>lua require'codeql.panel'.filter_shortest_path()<CR>]],
{ script = true, silent = true }
)
end
local function unhide_issues(issues)
for _, issue in ipairs(issues) do
issue.hidden = false
end
end
local function filter_issues(issues, filter_str)
for _, issue in ipairs(issues) do
if not is_filtered(filter_str, issue) then
issue.hidden = true
end
end
end
function M.filter_shortest_path()
local bufnr = vim.api.nvim_get_current_buf()
local panel = M.panels[bufnr]
local queries = panel.queries
for _, query in ipairs(queries) do
-- filter issues reaching the same sink, keeping only the shortest path
local issues = query.issues
local sinks = {}
for _, issue in ipairs(issues) do
issue.hidden = true
local key = issue.node.url.uri
.. ":"
.. issue.node.url.startLine
.. ":"
.. issue.node.url.startColumn
.. ":"
.. issue.node.url.endLine
.. ":"
.. issue.node.url.endColumn
if sinks[key] then
if sinks[key].min_path_length > issue.min_path_length then
sinks[key] = issue
end
else
sinks[key] = issue
end
end
for _, issue in pairs(sinks) do
issue.hidden = false
end
end
render_content(bufnr)
end
function M.clear_filter()
local bufnr = vim.api.nvim_get_current_buf()
local panel = M.panels[bufnr]
unhide_issues(panel.issues)
render_content(bufnr)
end
function M.label_filter()
local bufnr = vim.api.nvim_get_current_buf()
local panel = M.panels[bufnr]
local pattern = vim.fn.input "Pattern: "
unhide_issues(panel.issues)
filter_issues(
panel.issues,
string.format("string.match(string.lower(generate_issue_label(issue.node)), string.lower('%s')) ~= nil", pattern)
)
render_content(bufnr)
end
function M.generic_filter()
local bufnr = vim.api.nvim_get_current_buf()
local panel = M.panels[bufnr]
local pattern = vim.fn.input "Pattern: "
unhide_issues(panel.issues)
filter_issues(panel.issues, pattern)
render_content(bufnr)
end
function M.toggle_mode()
local bufnr = vim.api.nvim_get_current_buf()
local panel = M.panels[bufnr]
if panel.mode == "tree" then
panel.mode = "table"
elseif panel.mode == "table" then
panel.mode = "tree"
end
M.render {
issues = panel.issues,
source = panel.source,
mode = panel.mode,
columns = panel.columns,
panel_name = vim.fn.bufname(bufnr),
}
end
local function get_enclosing_issue(line)
local bufnr = vim.api.nvim_get_current_buf()
local line_map = M.panels[bufnr].line_map
local entry
while line >= 7 do
entry = line_map[line]
if
entry
and (entry.kind == "node" or entry.kind == "issue" or entry.kind == "query")
and vim.tbl_contains(vim.tbl_keys(entry.obj), "is_folded")
then
return line, entry.obj
end
line = line - 1
end
end
function M.toggle_fold()
local start_time = util.debug "Entering panel.toggle_fold()"
local bufnr = vim.api.nvim_get_current_buf()
-- prevent highlighting from being off after adding/removing the help text
vim.cmd "match none"
local line = vim.fn.line "."
local enc_line, issue = get_enclosing_issue(line)
if issue and vim.tbl_contains(vim.tbl_keys(issue), "is_folded") then
issue.is_folded = not issue.is_folded
render_keep_view(bufnr, line)
vim.api.nvim_win_set_cursor(0, { enc_line, 0 })
end
util.debug("Exiting panel.toggle_fold()", { start_time = start_time })
end
function M.toggle_help()
local start_time = util.debug "Entering panel.toggle_help()"
local bufnr = vim.api.nvim_get_current_buf()
panel_short_help = not panel_short_help
-- prevent highlighting from being off after adding/removing the help text
render_keep_view(bufnr)
util.debug("Exiting panel.toggle_help()", { start_time = start_time })
end
function M.set_fold_level(level)
local bufnr = vim.api.nvim_get_current_buf()
local line_map = M.panels[bufnr].line_map
for k, _ in pairs(line_map) do
line_map[k].obj.is_folded = level
end
render_keep_view(bufnr)
end
function M.change_path(offset)
local bufnr = vim.api.nvim_get_current_buf()
local line = vim.fn.line "."
local enc_line, issue = get_enclosing_issue(line)
if issue.active_path then
if issue.active_path == 1 and offset == -1 then
issue.active_path = #issue.paths
elseif issue.active_path == #issue.paths and offset == 1 then
issue.active_path = 1
else
issue.active_path = issue.active_path + offset
end
render_keep_view(bufnr, line + 1)
vim.api.nvim_win_set_cursor(0, { enc_line, 0 })
end
end
local function get_column_at_cursor(row)
local ranges = row.ranges
local cur = vim.api.nvim_win_get_cursor(0)
for i, range in ipairs(ranges) do
if tonumber(range[1]) <= tonumber(cur[2]) and tonumber(cur[2]) <= tonumber(range[2]) then
return row.columns[i]
end
end
end
local function get_current_node()
local bufnr = vim.api.nvim_get_current_buf()
local line_map = M.panels[bufnr].line_map
if not line_map[vim.fn.line "."] then
return
end
local node
local entry = line_map[vim.fn.line "."]
if entry.kind == "issue" then
node = entry.obj.node
elseif entry.kind == "node" then
node = entry.obj
elseif entry.kind == "row" then
node = get_column_at_cursor(entry.obj)
end
return node
end
function M.jump_to_code(stay_in_panel)
local node = get_current_node()
if not node or not node.visitable then
return
end
local filename = node.filename
if string.sub(filename, 1, 1) == "/" then
filename = string.sub(filename, 2)
end
local source, bufname, revisionId, nwo
if config.database.sourceArchiveZip and util.is_file(config.database.sourceArchiveZip) then
source = "source_archive"
-- create the ql:// buffer
bufname = string.format("ql://%s", filename)
elseif config.sarif.path and util.is_file(config.sarif.path) and config.sarif.hasArtifacts then
source = "sarif"
-- create the ql:// buffer
bufname = string.format("ql://%s", filename)
elseif config.database.sourceArchiveRoot and util.is_file(config.database.sourceArchiveRoot .. "/" .. filename) then
source = "file_system"
bufname = config.database.sourceArchiveRoot .. "/" .. filename
elseif util.is_file(vim.fn.getcwd() .. util.uri_to_fname("/" .. filename)) then
source = "file_system"
bufname = vim.fn.getcwd() .. "/" .. filename
elseif node.versionControlProvenance then
source = "vcs"
local repositoryUri = node.versionControlProvenance.repositoryUri
revisionId = node.versionControlProvenance.revisionId
nwo = vim.split(repositoryUri, "github.com/")[2]
bufname = string.format("ql://%s/%s/%s", nwo, revisionId, filename)
end
if not source then
util.err_message "Cannot figure out source code origin. Try setting up the database"
return
end
-- save audit pane window
local panel_winid = vim.fn.win_getid()
-- choose the target window to open the file in
local target_winid = require("window-picker").pick_window()
-- go to the target window
vim.fn.win_gotoid(target_winid)
local opts = {
nwo = nwo,
revisionId = revisionId,
line = node.line,
startLine = node.url.startLine,
endLine = node.url.endLine,
startColumn = node.url.startColumn,
endColumn = node.url.endColumn,
stay_in_panel = stay_in_panel,
panel_winid = panel_winid,
target_winid = target_winid,
range_ns = range_ns,
}
local bufnr = vim.fn.bufnr(bufname)
if bufnr > -1 then
-- buffer already exists, show it
vim.api.nvim_command(string.format("buffer %s", bufname))
if opts.line then
util.jump_to_line(opts)
end
if opts.startLine and opts.endLine and opts.startColumn and opts.endColumn then
util.highlight_range(bufnr, opts)
end
else
if source == "sarif" then
bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, bufname)
util.open_from_sarif(bufnr, filename, opts)
elseif source == "source_archive" then
bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, bufname)
util.open_from_archive(bufnr, filename, opts)
elseif source == "vcs" then
bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, bufname)
util.open_from_vcs(bufnr, filename, opts)
elseif source == "file_system" then
bufnr = vim.fn.bufnr(bufname, true)
end
end
if bufnr > -1 then
vim.api.nvim_win_set_buf(target_winid, bufnr)
else
util.err_message("Cannot find source code for " .. filename .. " in " .. source)
end
end
function M.preview_snippet()
local node = get_current_node()
if not node or not node.visitable then
return
end
local conf = config.values
local context_lines = conf.panel.context_lines
local snippet = {}
local max_len = 0
local context_start_line
if config.sarif.path and node.contextRegion and node.contextRegion.snippet then
snippet = vim.split(node.contextRegion.snippet.text, "\n")
context_start_line = node.contextRegion.startLine
for _, line in ipairs(snippet) do
if #line > max_len then
max_len = #line
end
end
elseif config.sarif.path and config.sarif.hasArtifacts then
local sarif = util.read_json_file(config.sarif.path)
local artifacts = sarif.runs[1].artifacts
for _, artifact in ipairs(artifacts) do
local uri = "/" .. util.uri_to_fname(artifact.location.uri)
if uri == node.filename then
local content = vim.split(artifact.contents.text, "\n")
context_start_line = math.max(node.url.startLine - context_lines, 0)
local context_end_line = math.min(node.url.startLine + context_lines, #content)
for i = context_start_line, context_end_line do
table.insert(snippet, content[i])
if #content[i] > max_len then
max_len = #content[i]
end
end
break
end
end
elseif config.database.sourceArchiveZip then
local zipfile = config.database.sourceArchiveZip
local path = string.gsub(node.filename, "^/", "")
local content = vim.fn.systemlist(string.format("unzip -p -- %s %s", zipfile, path))
context_start_line = math.max(node.url.startLine - context_lines, 0)
local context_end_line = math.min(node.url.startLine + context_lines, #content)
for i = context_start_line, context_end_line do
table.insert(snippet, content[i])
if #content[i] > max_len then
max_len = #content[i]
end
end
end
-- create popup window
local popup = Popup {
enter = false,
focusable = false,
relative = "editor",
border = {
style = "rounded",
},
position = "50%",
size = {
width = max_len + 10,
height = #snippet + 2,
},
buf_options = {
modifiable = true,
readonly = false,
filetype = "markdown",
},
win_options = {
winblend = 10,
winhighlight = "Normal:NormalAlt,FloatBorder:FloatBorder",
},
}