-
Notifications
You must be signed in to change notification settings - Fork 3
/
todotreeview.lua
740 lines (651 loc) · 20.3 KB
/
todotreeview.lua
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
local core = require "core"
local common = require "core.common"
local command = require "core.command"
local config = require "core.config"
local keymap = require "core.keymap"
local style = require "core.style"
local View = require "core.view"
local StatusView = require "core.statusview"
local CommandView = require "core.commandview"
local DocView = require "core.docview"
local TodoTreeView = View:extend()
config.todo_tags = {"TODO", "BUG", "FIX", "FIXME", "IMPROVEMENT"}
config.tag_colors = {
TODO = {tag=style.text, tag_hover=style.accent, text=style.text, text_hover=style.accent},
BUG = {tag=style.text, tag_hover=style.accent, text=style.text, text_hover=style.accent},
FIX = {tag=style.text, tag_hover=style.accent, text=style.text, text_hover=style.accent},
FIXME = {tag=style.text, tag_hover=style.accent, text=style.text, text_hover=style.accent},
IMPROVEMENT = {tag=style.text, tag_hover=style.accent, text=style.text, text_hover=style.accent},
}
config.todo_file_color = {
name = style.text,
hover = style.accent
}
-- Paths or files to be ignored
config.ignore_paths = {}
-- Tells if the plugin should start with the nodes expanded
config.todo_expanded = true
-- 'tag' mode can be used to group the todos by tags
-- 'file' mode can be used to group the todos by files
-- 'file_tag' mode can be used to group the todos by files and then by tags inside the files
config.todo_mode = "tag"
-- Only used in file mode when the tag and the text are on the same line
config.todo_separator = " - "
-- Text displayed when the note is empty
config.todo_default_text = "blank"
local SCOPES = {
ALL = "all",
FOCUSED = "focused",
}
-- Scope of the displayed tags
-- 'all' scope to show all tags from the project all the time
-- 'focused' scope to show the tags from the currently focused file
config.todo_scope = SCOPES.FOCUSED
function TodoTreeView:new()
TodoTreeView.super.new(self)
self.scrollable = true
self.focusable = false
self.visible = true
self.times_cache = {}
self.cache = {}
self.cache_updated = false
self.init_size = true
self.focus_index = 0
self.filter = ""
self.previous_focused_file = nil
-- Items are generated from cache according to the mode
self.items = {}
end
local function is_file_ignored(filename)
for _, path in ipairs(config.ignore_paths) do
local s, _ = filename:find(path)
if s then
return true
end
end
return false
end
function TodoTreeView:is_file_in_scope(filename)
if config.todo_scope == SCOPES.ALL then
return true
elseif config.todo_scope == SCOPES.FOCUSED then
if core.active_view:is(DocView) then
if core.active_view:is(CommandView) then
if self.previous_focused_file == nil then
return true
else
return self.previous_focused_file == filename
end
else
return core.active_view.doc.filename == filename
end
elseif core.active_view:is(TodoTreeView) then
if self.previous_focused_file == nil then
return true
else
return self.previous_focused_file == filename
end
else
return true
end
else
assert(false, "Unknown scope defined ("..config.todo_scope..")")
end
end
function TodoTreeView:get_all_files()
local all_files = {}
for _, file in ipairs(core.project_files) do
if file.filename then
all_files[file.filename] = file
end
end
for _, file in ipairs(core.docs) do
if file.filename and not all_files[file.filename] then
all_files[file.filename] = {
filename = file.filename,
type = "file"
}
end
end
return all_files
end
function TodoTreeView:refresh_cache()
local items = {}
if not next(self.items) then
items = self.items
end
self.updating_cache = true
core.add_thread(function()
for _, item in pairs(self:get_all_files()) do
local ignored = is_file_ignored(item.filename)
if not ignored and item.type == "file" then
local cached = self:get_cached(item)
if config.todo_mode == "file" then
items[cached.filename] = cached
elseif config.todo_mode == "file_tag" then
local file_t = {}
file_t.expanded = config.todo_expanded
file_t.type = "file"
file_t.tags = {}
file_t.todos = {}
file_t.filename = cached.filename
file_t.abs_filename = cached.abs_filename
items[cached.filename] = file_t
for _, todo in ipairs(cached.todos) do
local tag = todo.tag
if not file_t.tags[tag] then
local tag_t = {}
tag_t.expanded = config.todo_expanded
tag_t.type = "group"
tag_t.todos = {}
tag_t.tag = tag
file_t.tags[tag] = tag_t
end
table.insert(file_t.tags[tag].todos, todo)
end
else
for _, todo in ipairs(cached.todos) do
local tag = todo.tag
if not items[tag] then
local t = {}
t.expanded = config.todo_expanded
t.type = "group"
t.todos = {}
t.tag = tag
items[tag] = t
end
table.insert(items[tag].todos, todo)
end
end
end
end
-- Copy expanded from old items
if config.todo_mode == "tag" and next(self.items) then
for tag, data in pairs(self.items) do
if items[tag] then
items[tag].expanded = data.expanded
end
end
end
self.items = items
core.redraw = true
self.cache_updated = true
self.updating_cache = false
end, self)
end
local function find_file_todos(t, filename)
local fp = io.open(filename)
if not fp then return t end
local n = 1
for line in fp:lines() do
for _, todo_tag in ipairs(config.todo_tags) do
-- Add spaces at the start and end of line so the pattern will pick
-- tags at the start and at the end of lines
local extended_line = " "..line.." "
local match_str = "[^a-zA-Z_\"'`]"..todo_tag.."[^\"'a-zA-Z_`]+"
local s, e = extended_line:find(match_str)
if s then
local d = {}
d.type = "todo"
d.tag = todo_tag
d.filename = filename
d.text = extended_line:sub(e+1)
if d.text == "" then
d.text = config.todo_default_text
end
d.line = n
d.col = s
table.insert(t, d)
end
core.redraw = true
end
if n % 100 == 0 then coroutine.yield() end
n = n + 1
core.redraw = true
end
fp:close()
end
function TodoTreeView:get_cached(item)
local t = self.cache[item.filename]
if not t then
t = {}
t.expanded = config.todo_expanded
t.filename = item.filename
t.abs_filename = system.absolute_path(item.filename)
t.type = item.type
t.todos = {}
t.tags = {}
find_file_todos(t.todos, t.filename)
self.cache[t.filename] = t
end
return t
end
function TodoTreeView:get_name()
return "Todo Tree"
end
function TodoTreeView:get_item_height()
return style.font:get_height() + style.padding.y
end
function TodoTreeView:get_cached_time(doc)
local t = self.times_cache[doc]
if not t then
local info = system.get_file_info(doc.filename)
if not info then return nil end
self.times_cache[doc] = info.modified
end
return t
end
function TodoTreeView:check_cache()
local existing_docs = {}
for _, doc in ipairs(core.docs) do
if doc.filename then
existing_docs[doc.filename] = true
local info = system.get_file_info(doc.filename)
local cached = self:get_cached_time(doc)
if not info and cached then
-- document deleted
self.times_cache[doc] = nil
self.cache[doc.filename] = nil
self.cache_updated = false
elseif cached and cached ~= info.modified then
-- document modified
self.times_cache[doc] = info.modified
self.cache[doc.filename] = nil
self.cache_updated = false
elseif not cached then
self.cache_updated = false
end
end
end
for _, file in ipairs(core.project_files) do
existing_docs[file.filename] = true
end
-- Check for docs in cache that may not exist anymore
-- for example: (Openend from outside of project and closed)
for filename, doc in pairs(self.cache) do
local exists = existing_docs[filename]
if not exists then
self.times_cache[doc] = nil
self.cache[filename] = nil
self.cache_updated = false
end
end
if core.project_files ~= self.last_project_files then
self.last_project_files = core.project_files
self.cache_updated = false
end
end
function TodoTreeView:each_item()
self:check_cache()
if not self.updating_cache and not self.cache_updated then
self:refresh_cache()
end
return coroutine.wrap(function()
local ox, oy = self:get_content_offset()
local y = oy + style.padding.y
local w = self.size.x
local h = self:get_item_height()
for _, item in pairs(self.items) do
local in_scope = item.type == "group" or self:is_file_in_scope(item.filename)
if in_scope and #item.todos > 0 then
coroutine.yield(item, ox, y, w, h)
y = y + h
for _, todo in ipairs(item.todos) do
if item.expanded then
local in_todo = string.find(todo.text:lower(), self.filter:lower())
local todo_in_scope = self:is_file_in_scope(todo.filename)
if todo_in_scope and (#self.filter == 0 or in_todo) then
coroutine.yield(todo, ox, y, w, h)
y = y + h
end
end
end
end
if in_scope and item.tags then
local first_tag = true
for _, tag in pairs(item.tags) do
if first_tag then
coroutine.yield(item, ox, y, w, h)
y = y + h
first_tag = false
end
if item.expanded then
coroutine.yield(tag, ox, y, w, h)
y = y + h
for _, todo in ipairs(tag.todos) do
if item.expanded and tag.expanded then
local in_todo = string.find(todo.text:lower(), self.filter:lower())
local todo_in_scope = self:is_file_in_scope(todo.filename)
if todo_in_scope and (#self.filter == 0 or in_todo) then
coroutine.yield(todo, ox, y, w, h)
y = y + h
end
end
end
end
end
end
end
end)
end
function TodoTreeView:on_mouse_moved(px, py)
self.hovered_item = nil
for item, x,y,w,h in self:each_item() do
if px > x and py > y and px <= x + w and py <= y + h then
self.hovered_item = item
break
end
end
end
function TodoTreeView:goto_hovered_item()
if not self.hovered_item then
return
end
if self.hovered_item.type == "group" or self.hovered_item.type == "file" then
return
end
core.try(function()
local i = self.hovered_item
local dv = core.root_view:open_doc(core.open_doc(i.filename))
core.root_view.root_node:update_layout()
dv.doc:set_selection(i.line, i.col)
dv:scroll_to_line(i.line, false, true)
end)
end
function TodoTreeView:on_mouse_pressed(button, x, y)
if not self.hovered_item then
return
elseif self.hovered_item.type == "file"
or self.hovered_item.type == "group" then
self.hovered_item.expanded = not self.hovered_item.expanded
else
self:goto_hovered_item()
end
end
function TodoTreeView:update()
-- Update focus
if core.active_view:is(DocView) then
if not core.active_view:is(CommandView) then
self.previous_focused_file = core.active_view.doc.filename
end
elseif core.active_view:is(TodoTreeView) then
else
self.previous_focused_file = nil
end
self.scroll.to.y = math.max(0, self.scroll.to.y)
-- update width
local dest = self.visible and config.treeview_size or 0
if self.init_size then
self.size.x = dest
self.init_size = false
else
self:move_towards(self.size, "x", dest)
end
TodoTreeView.super.update(self)
end
function TodoTreeView:draw()
self:draw_background(style.background2)
--local h = self:get_item_height()
local icon_width = style.icon_font:get_width("D")
local spacing = style.font:get_width(" ") * 2
local root_depth = 0
for item, x,y,w,h in self:each_item() do
local text_color = style.text
local tag_color = style.text
local file_color = config.todo_file_color.name or style.text
if config.tag_colors[item.tag] then
text_color = config.tag_colors[item.tag].text or style.text
tag_color = config.tag_colors[item.tag].tag or style.text
end
-- hovered item background
if item == self.hovered_item then
renderer.draw_rect(x, y, w, h, style.line_highlight)
text_color = style.accent
tag_color = style.accent
file_color = config.todo_file_color.hover or style.accent
if config.tag_colors[item.tag] then
text_color = config.tag_colors[item.tag].text_hover or style.accent
tag_color = config.tag_colors[item.tag].tag_hover or style.accent
end
end
-- icons
local item_depth = 0
x = x + (item_depth - root_depth) * style.padding.x + style.padding.x
if item.type == "file" then
local icon1 = item.expanded and "-" or "+"
common.draw_text(style.icon_font, file_color, icon1, nil, x, y, 0, h)
x = x + style.padding.x
common.draw_text(style.icon_font, file_color, "f", nil, x, y, 0, h)
x = x + icon_width
elseif item.type == "group" then
if config.todo_mode == "file_tag" then
x = x + style.padding.x * 0.75
end
local icon1 = item.expanded and "-" or ">"
common.draw_text(style.icon_font, tag_color, icon1, nil, x, y, 0, h)
x = x + icon_width / 2
else
if config.todo_mode == "tag" then
x = x + style.padding.x
else
x = x + style.padding.x * 1.5
end
common.draw_text(style.icon_font, text_color, "i", nil, x, y, 0, h)
x = x + icon_width
end
-- text
x = x + spacing
if item.type == "file" then
common.draw_text(style.font, file_color, item.filename, nil, x, y, 0, h)
elseif item.type == "group" then
common.draw_text(style.font, tag_color, item.tag, nil, x, y, 0, h)
else
if config.todo_mode == "file" then
common.draw_text(style.font, tag_color, item.tag, nil, x, y, 0, h)
x = x + style.font:get_width(item.tag)
common.draw_text(style.font, text_color, config.todo_separator..item.text, nil, x, y, 0, h)
else
common.draw_text(style.font, text_color, item.text, nil, x, y, 0, h)
end
end
end
end
function TodoTreeView:get_item_by_index(index)
local i = 0
for item in self:each_item() do
if index == i then
return item
end
i = i + 1
end
return nil
end
function TodoTreeView:get_hovered_parent_file_tag()
local file_parent = nil
local file_parent_index = 0
local group_parent = nil
local group_parent_index = 0
local i = 0
for item in self:each_item() do
if item.type == "file" then
file_parent = item
file_parent_index = i
end
if item.type == "group" then
group_parent = item
group_parent_index = i
end
if i == self.focus_index then
if item.type == "file" or item.type == "group" then
return file_parent, file_parent_index
else
return group_parent, group_parent_index
end
end
i = i + 1
end
return nil, 0
end
function TodoTreeView:get_hovered_parent()
local parent = nil
local parent_index = 0
local i = 0
for item in self:each_item() do
if item.type == "group" or item.type == "file" then
parent = item
parent_index = i
end
if i == self.focus_index then
return parent, parent_index
end
i = i + 1
end
return nil, 0
end
function TodoTreeView:update_scroll_position()
local h = self:get_item_height()
local _, min_y, _, max_y = self:get_content_bounds()
local start_row = math.floor(min_y / h)
local end_row = math.floor(max_y / h)
if self.focus_index < start_row then
self.scroll.to.y = self.focus_index * h
end
if self.focus_index + 1 > end_row then
self.scroll.to.y = (self.focus_index * h) - self.size.y + h
end
end
-- init
local view = TodoTreeView()
local node = core.root_view:get_active_node()
view.size.x = config.treeview_size
node:split("right", view, true)
local get_items = StatusView.get_items
function StatusView:get_items()
local left, right = get_items(self)
if not core.active_view:is(CommandView) and #view.filter > 0 then
table.insert(right, 1, StatusView.separator)
table.insert(right, 1, string.format("Filter: %s", view.filter))
end
return left, right
end
-- register commands and keymap
local previous_view = nil
command.add(nil, {
["todotreeview:toggle"] = function()
view.visible = not view.visible
end,
["todotreeview:expand-items"] = function()
for _, item in pairs(view.items) do
item.expanded = true
end
end,
["todotreeview:hide-items"] = function()
for _, item in pairs(view.items) do
item.expanded = false
end
end,
["todotreeview:toggle-focus"] = function()
if not core.active_view:is(TodoTreeView) then
previous_view = core.active_view
core.set_active_view(view)
view.hovered_item = view:get_item_by_index(view.focus_index)
else
command.perform("todotreeview:release-focus")
end
end,
["todotreeview:filter-notes"] = function()
local todo_view_focus = core.active_view:is(TodoTreeView)
local previous_filter = view.filter
core.command_view:set_text(view.filter, true)
local submit = function(text)
view.filter = text
if todo_view_focus then
view.focus_index = 0
view.hovered_item = view:get_item_by_index(view.focus_index)
view:update_scroll_position()
end
end
local suggest = function(text)
view.filter = text
end
local cancel = function(explicit)
view.filter = previous_filter
end
core.command_view:enter("Filter Notes", submit, suggest, cancel)
end,
})
command.add(
function()
return core.active_view:is(TodoTreeView)
end, {
["todotreeview:previous"] = function()
if view.focus_index > 0 then
view.focus_index = view.focus_index - 1
view.hovered_item = view:get_item_by_index(view.focus_index)
view:update_scroll_position()
end
end,
["todotreeview:next"] = function()
local next_index = view.focus_index + 1
local next_item = view:get_item_by_index(next_index)
if next_item then
view.focus_index = next_index
view.hovered_item = next_item
view:update_scroll_position()
end
end,
["todotreeview:collapse"] = function()
if not view.hovered_item then
return
end
if view.hovered_item.type == "file" then
view.hovered_item.expanded = false
else
if view.hovered_item.type == "group" and view.hovered_item.expanded then
view.hovered_item.expanded = false
else
if config.todo_mode == "file_tag" then
view.hovered_item, view.focus_index = view:get_hovered_parent_file_tag()
else
view.hovered_item, view.focus_index = view:get_hovered_parent()
end
view:update_scroll_position()
end
end
end,
["todotreeview:expand"] = function()
if not view.hovered_item then
return
end
if view.hovered_item.type == "file" or view.hovered_item.type == "group" then
if view.hovered_item.expanded then
command.perform("todotreeview:next")
else
view.hovered_item.expanded = true
end
end
end,
["todotreeview:open"] = function()
if not view.hovered_item then
return
end
view:goto_hovered_item()
view.hovered_item = nil
end,
["todotreeview:release-focus"] = function()
core.set_active_view(
previous_view or core.root_view:get_primary_node().active_view
)
view.hovered_item = nil
end,
})
keymap.add { ["ctrl+shift+t"] = "todotreeview:toggle" }
keymap.add { ["ctrl+shift+e"] = "todotreeview:expand-items" }
keymap.add { ["ctrl+shift+h"] = "todotreeview:hide-items" }
keymap.add { ["ctrl+shift+b"] = "todotreeview:filter-notes" }
keymap.add { ["up"] = "todotreeview:previous" }
keymap.add { ["down"] = "todotreeview:next" }
keymap.add { ["left"] = "todotreeview:collapse" }
keymap.add { ["right"] = "todotreeview:expand" }
keymap.add { ["return"] = "todotreeview:open" }
keymap.add { ["escape"] = "todotreeview:release-focus" }