From 0ebc0e789d1585ef1b17a31afeedf7b5d1ff20e2 Mon Sep 17 00:00:00 2001 From: George Sokianos Date: Thu, 26 Dec 2024 20:43:53 +0000 Subject: [PATCH] Added and updated many plugins --- README_Amiga.md | 35 +- resources/amiga/addons/plugins/amimodkeys.lua | 19 + .../amiga/addons/plugins/indentguide.lua | 53 +- resources/amiga/addons/plugins/keyhud.lua | 192 ++++ .../amiga/addons/plugins/memoryusage.lua | 49 ++ resources/amiga/addons/plugins/minimap.lua | 247 +++--- resources/amiga/addons/plugins/sort.lua | 31 + resources/amiga/addons/plugins/sortcss.lua | 413 +++++++++ .../amiga/addons/plugins/todotreeview-xl.lua | 820 ++++++++++++++++++ 9 files changed, 1695 insertions(+), 164 deletions(-) create mode 100644 resources/amiga/addons/plugins/amimodkeys.lua create mode 100644 resources/amiga/addons/plugins/keyhud.lua create mode 100644 resources/amiga/addons/plugins/memoryusage.lua create mode 100644 resources/amiga/addons/plugins/sort.lua create mode 100644 resources/amiga/addons/plugins/sortcss.lua create mode 100644 resources/amiga/addons/plugins/todotreeview-xl.lua diff --git a/README_Amiga.md b/README_Amiga.md index b490d9a3..95ede554 100644 --- a/README_Amiga.md +++ b/README_Amiga.md @@ -70,6 +70,10 @@ need. The included plugins are the following: +**amimodkeys** +This plugin enables the Right Amiga and Right Alt to behave like Control, +for those that are used to use them. + **autoinsert** Automatically inserts closing brackets and quotes. Also allows selected text to be wrapped with brackets or quotes. @@ -124,6 +128,9 @@ folder like below: **indentguide** Adds indent guides +**keyhud** +Simple key HUD display for lite-xl. + **language_guide** Syntax for the AmigaGuide scripting language @@ -142,6 +149,9 @@ Automatically inserts indentation and closing bracket/text after newline **markers** Add markers to docs and jump between them quickly +**memoryusage** +Show memory usage in the status view + **minimap** Shows a minimap on the right-hand side of the docview. Please note that this plugin will make the editor slower on file loading and scrolling. @@ -178,9 +188,19 @@ Highlights regions of code that match the current selection **smallclock** It adds a small clock at the bottom right corner. +**sort** +Sorts selected lines alphabetically + +**sortcss** +Sort selected CSS properties alphabetically or using the concentric model. + **tetris** Play Tetris inside Lite XL. +**todotreeview** +Todo tree viewer for annotations in code like TODO, BUG, FIX, +IMPROVEMENT + ## Tips and tricks ### Transitions @@ -231,12 +251,21 @@ https://git.walkero.gr/walkero/lite-xl/issues # Changelog ## [2.1.7r1] - 2024-12-26 ### Added -- Added widget library -- Added settings plugin that shows a GUI for chnaging the app settings -- Added search_ui plugin that adds a GUI for search +- Added the widget library +- Added the settings plugin that shows a GUI for chnaging the app settings +- Added the search_ui plugin that adds a GUI for search +- Added the amimodkeys plugin that the Right Amiga and Right Alt to behave + like Control +- Added the memoryusage plugin +- Added the todotreeview plugin +- Added the keyhud plugin +- Added the sort plugin +- Added the sortcss plugin ### Updated - Updated the code to the upstream 2.1.7 release +- Updated the minimap plugin +- Updated the indentguide plugin ## [2.1.6r1] - 2024-12-3 ### Changed diff --git a/resources/amiga/addons/plugins/amimodkeys.lua b/resources/amiga/addons/plugins/amimodkeys.lua new file mode 100644 index 00000000..a9d77850 --- /dev/null +++ b/resources/amiga/addons/plugins/amimodkeys.lua @@ -0,0 +1,19 @@ +-- mod-version:3 +local keymap = require "core.keymap" + +local on_key_pressed = keymap.on_key_pressed +local on_key_released = keymap.on_key_released + +local function remap_key(k) + return k:gsub("right alt", "control") + :gsub("right amiga", "control") +end + +function keymap.on_key_pressed(k, ...) + return on_key_pressed(remap_key(k), ...) +end + +function keymap.on_key_released(k, ...) + return on_key_released(remap_key(k), ...) +end + diff --git a/resources/amiga/addons/plugins/indentguide.lua b/resources/amiga/addons/plugins/indentguide.lua index 42eb3a6e..1cb4f8d0 100644 --- a/resources/amiga/addons/plugins/indentguide.lua +++ b/resources/amiga/addons/plugins/indentguide.lua @@ -6,6 +6,7 @@ local DocView = require "core.docview" config.plugins.indentguide = common.merge({ enabled = true, + highlight = true, -- The config specification used by the settings gui config_spec = { name = "Indent Guide", @@ -15,28 +16,28 @@ config.plugins.indentguide = common.merge({ path = "enabled", type = "toggle", default = true + }, + { + label = "Highlight Line", + description = "Toggle the highlight of the curent indentation indicator lines.", + path = "highlight", + type = "toggle", + default = true } } }, config.plugins.indentguide) --- TODO: replace with `doc:get_indent_info()` when 2.1 releases -local function get_indent_info(doc) - if doc.get_indent_info then - return doc:get_indent_info() - end - return config.tab_type, config.indent_size -end +local indentguide = {} - -local function get_line_spaces(doc, line, dir) - local _, indent_size = get_indent_info(doc) +function indentguide.get_line_spaces(doc, line, dir) + local _, indent_size = doc:get_indent_info() local text = doc.lines[line] if not text or #text == 1 then return -1 end local s, e = text:find("^%s*") if e == #text then - return get_line_spaces(doc, line + dir, dir) + return indentguide.get_line_spaces(doc, line + dir, dir) end local n = 0 for _,b in pairs({text:byte(s, e)}) do @@ -46,15 +47,16 @@ local function get_line_spaces(doc, line, dir) end -local function get_line_indent_guide_spaces(doc, line) +function indentguide.get_line_indent_guide_spaces(doc, line) if doc.lines[line]:find("^%s*\n") then return math.max( - get_line_spaces(doc, line - 1, -1), - get_line_spaces(doc, line + 1, 1)) + indentguide.get_line_spaces(doc, line - 1, -1), + indentguide.get_line_spaces(doc, line + 1, 1)) end - return get_line_spaces(doc, line) + return indentguide.get_line_spaces(doc, line) end + local docview_update = DocView.update function DocView:update() docview_update(self) @@ -66,7 +68,7 @@ function DocView:update() local function get_indent(line) if line < 1 or line > #self.doc.lines then return -1 end if not self.indentguide_indents[line] then - self.indentguide_indents[line] = get_line_indent_guide_spaces(self.doc, line) + self.indentguide_indents[line] = indentguide.get_line_indent_guide_spaces(self.doc, line) end return self.indentguide_indents[line] end @@ -76,10 +78,10 @@ function DocView:update() local minline, maxline = self:get_visible_line_range() for i = minline, maxline do - self.indentguide_indents[i] = get_line_indent_guide_spaces(self.doc, i) + self.indentguide_indents[i] = indentguide.get_line_indent_guide_spaces(self.doc, i) end - local _, indent_size = get_indent_info(self.doc) + local _, indent_size = self.doc:get_indent_info() for _,line in self.doc:get_selections() do local lvl = get_indent(line) local top, bottom @@ -121,19 +123,26 @@ function DocView:update() end +function indentguide.get_width() + return math.max(1, SCALE) +end + + local draw_line_text = DocView.draw_line_text function DocView:draw_line_text(line, x, y) if config.plugins.indentguide.enabled and self:is(DocView) then local spaces = self.indentguide_indents[line] or -1 - local _, indent_size = get_indent_info(self.doc) - local w = math.max(1, SCALE) + local _, indent_size = self.doc:get_indent_info() + local w = indentguide.get_width() local h = self:get_line_height() local font = self:get_font() local space_sz = font:get_width(" ") for i = 0, spaces - 1, indent_size do local color = style.guide or style.selection local active_lvl = self.indentguide_indent_active[line] or -1 - if i < active_lvl and i + indent_size >= active_lvl then + if i < active_lvl + and i + indent_size >= active_lvl + and config.plugins.indentguide.highlight then color = style.guide_highlight or style.accent end local sw = space_sz * i @@ -142,3 +151,5 @@ function DocView:draw_line_text(line, x, y) end return draw_line_text(self, line, x, y) end + +return indentguide diff --git a/resources/amiga/addons/plugins/keyhud.lua b/resources/amiga/addons/plugins/keyhud.lua new file mode 100644 index 00000000..5a59b358 --- /dev/null +++ b/resources/amiga/addons/plugins/keyhud.lua @@ -0,0 +1,192 @@ +-- mod-version:3 +local core = require "core" +local keymap = require "core.keymap" +local style = require "core.style" +local CommandView = require "core.commandview" +local RootView = require "core.rootview" +local config = require "core.config" +local common = require "core.common" + + +local keyhud = {} + +config.plugins.keyhud = common.merge({ + stroke_map = { + ["escape"] = "", + ["space"] = "", --"␣"" + ["left gui"] = "", --"⌘" + ["right gui"] = "", + ["left ctrl"] = "", + ["control"] = "", + ["right ctrl"] = "", + ["left alt"] = "", + ["right alt"] = "", + ["left amiga"] = "", + ["right amiga"] = "", + ["left"] = "←", + ["right"] = "→", + ["up"] = "↑", + ["down"] = "↓", + ["left shift"] = "⇧", + ["right shift"] = "⇧", + ["capslock"] = "⇪", + ["return"] = "", --"↵", + ["backspace"] = "⌫", + ["delete"] = "⌦", + ["pageup"] = "", --"⇞", + ["pagedown"] = "", --"⇟", + ["home"] = "", --"↖", + ["end"] = "", --"↘", + ["tab"] = "", --"⇥", + }, + max_time = 0.5, + only_mapped = false, + filters = { + ["commandview"] = true, + ["mouse"] = true + }, + position = "right", +}, config.plugins.keyhud) + +style.keyhud = common.merge( + { + background = { common.color "#00000066" }, + text = { common.color "#ffffffdd" }, + font = style.big_font, -- style.code_font:copy(46 * SCALE) + }, + style.keyhud +) + +keyhud.last_strokes = {} +keyhud.last_strokes_time_stamp = {} + + +keyhud.on_key_pressed__orig = keymap.on_key_pressed +keyhud.on_key_released__orig = keymap.on_key_released + + +local function dv() + return core.active_view +end + +function keymap.on_key_pressed(k, ...) + if dv():is(CommandView) and config.plugins.keyhud.filters.commandview then + return keyhud.on_key_pressed__orig(k, ...) + end + if config.plugins.keyhud.filters.mouse and (string.find(k, "click", 1, true) or string.find(k, "wheel", 1, true)) then + return keyhud.on_key_pressed__orig(k, ...) + end + local x = config.plugins.keyhud.stroke_map[k] + if x == nil and not config.plugins.keyhud.only_mapped then + if #k > 1 then + x = '<' .. k .. '>' + else + x = k + end + end + if x ~= nil then + for i, key in ipairs(keyhud.last_strokes) do + if x == key then + keyhud.last_strokes_time_stamp[i] = -1 + x = nil + break + end + end + end + if x ~= nil then + table.insert(keyhud.last_strokes, x) + table.insert(keyhud.last_strokes_time_stamp, -1) + end + return keyhud.on_key_pressed__orig(k, ...) +end + +function keymap.on_key_released(k) + if #keyhud.last_strokes then + local x = config.plugins.keyhud.stroke_map[k] + if x == nil then + x = k + end + for i, key in ipairs(keyhud.last_strokes) do + if x == key then + keyhud.last_strokes_time_stamp[i] = system.get_time() + break + end + end + end + return keyhud.on_key_released__orig(k) +end + +local rvDraw = RootView.draw +function RootView:draw(...) + rvDraw(self, ...) + local position = config.plugins.keyhud.position + if position ~= 'right' and position ~= 'left' then + core.error("`config.plugins.keyhud.position` can be only `left` or `right`") + return nil + end + local font = style.keyhud.font + local h = font:get_height() + 20 + local w = h + local y = self.size.y - 10 + local next_strokes = {} + local next_timestamps = {} + local start_i, end_i, step = 0, 0, 1 + if position == "left" then + local x = 10 + for i = 1, #keyhud.last_strokes do + local t0 = keyhud.last_strokes_time_stamp[i] + if t0 < 0 or system.get_time() - t0 < config.plugins.keyhud.max_time then + local key = keyhud.last_strokes[i] + core.redraw = true + -- y = self.size.y - core.status_view.size.y + local tw = font:get_width(key) + local th = font:get_height() + w = h + if tw + 20 > w then + w = tw + 20 + end + renderer.draw_rect(x, y - h, w, h, style.keyhud.background) + renderer.draw_text(font, key, x + w / 2 - tw / 2, y - h / 2 - th / 2, + style.keyhud.text) + x = x + w + 10 + table.insert(next_strokes, key) + table.insert(next_timestamps, t0) + end + end + start_i = 1 + end_i = #next_strokes + step = 1 + else + local x = self.size.x - 10 + for i = #keyhud.last_strokes, 1, -1 do + local t0 = keyhud.last_strokes_time_stamp[i] + if t0 < 0 or system.get_time() - t0 < config.plugins.keyhud.max_time then + local key = keyhud.last_strokes[i] + core.redraw = true + -- y = self.size.y - core.status_view.size.y + local tw = font:get_width(key) + local th = font:get_height() + if tw + 20 > w then + w = tw + 20 + end + renderer.draw_rect(x - w, y - h, w, h, style.keyhud.background) + renderer.draw_text(font, key, x - w / 2 - tw / 2, y - h / 2 - th / 2, + style.keyhud.text) + x = x - w - 10 + table.insert(next_strokes, key) + table.insert(next_timestamps, t0) + end + end + start_i = #next_strokes + end_i = 1 + step = -1 + end + keyhud.last_strokes = {} + keyhud.last_strokes_time_stamp = {} + for i = start_i, end_i, step do + table.insert(keyhud.last_strokes, next_strokes[i]) + table.insert(keyhud.last_strokes_time_stamp, next_timestamps[i]) + end +end + +return keyhud diff --git a/resources/amiga/addons/plugins/memoryusage.lua b/resources/amiga/addons/plugins/memoryusage.lua new file mode 100644 index 00000000..290b4e11 --- /dev/null +++ b/resources/amiga/addons/plugins/memoryusage.lua @@ -0,0 +1,49 @@ +-- mod-version:3 +-- original implementation by AqilCont +local core = require "core" +local config = require "core.config" +local common = require "core.common" +local style = require "core.style" +local StatusView = require "core.statusview" + +config.plugins.memoryusage = common.merge({ + enabled = true, + -- The config specification used by the settings gui + config_spec = { + name = "Memory Usage", + { + label = "Enabled", + description = "Show or hide the lua memory usage from the status bar.", + path = "enabled", + type = "toggle", + default = true, + on_apply = function(enabled) + core.add_thread(function() + if enabled then + core.status_view:get_item("status:memory-usage"):show() + else + core.status_view:get_item("status:memory-usage"):hide() + end + end) + end + } + } +}, config.plugins.memoryusage) + +core.status_view:add_item({ + name = "status:memory-usage", + alignment = StatusView.Item.RIGHT, + get_item = function() + return { + style.text, + string.format( + "%.2f MB", + (math.floor(collectgarbage("count") / 10.24) / 100) + ) + } + end, + position = 1, + tooltip = "lua memory usage", + separator = core.status_view.separator2 +}) + diff --git a/resources/amiga/addons/plugins/minimap.lua b/resources/amiga/addons/plugins/minimap.lua index 75eb272e..6dd2a1cb 100644 --- a/resources/amiga/addons/plugins/minimap.lua +++ b/resources/amiga/addons/plugins/minimap.lua @@ -135,29 +135,21 @@ config.plugins.minimap = common.merge({ }, { label = "Selection Color", - description = "Background color of selected text in html notation eg: #FFFFFF. Leave empty to use default.", - path = "selection_color_html", - type = "string", - on_apply = function(value) - if value and value:match("#%x%x%x%x%x%x") then - config.plugins.minimap.selection_color = { common.color(value) } - else - config.plugins.minimap.selection_color = nil - end - end + description = "Background color of selected text.", + path = "selection_color", + type = "color", + default = string.format("#%02X%02X%02X%02X", + style.dim[1], style.dim[2], style.dim[3], style.dim[4] + ) }, { label = "Caret Color", - description = "Background color of active line in html notation eg: #FFFFFF. Leave empty to use default.", - path = "caret_color_html", - type = "string", - on_apply = function(value) - if value and value:match("#%x%x%x%x%x%x") then - config.plugins.minimap.caret_color = { common.color(value) } - else - config.plugins.minimap.caret_color = nil - end - end + description = "Background color of active line.", + path = "caret_color", + type = "color", + default = string.format("#%02X%02X%02X%02X", + style.caret[1], style.caret[2], style.caret[3], style.caret[4] + ) }, { label = "Highlight Alignment", @@ -236,8 +228,6 @@ local function reset_cache_if_needed() end - - -- Move cache to make space for new lines local prev_insert_notify = Highlighter.insert_notify function Highlighter:insert_notify(line, n, ...) @@ -246,13 +236,11 @@ function Highlighter:insert_notify(line, n, ...) if not highlighter_cache[self] then highlighter_cache[self] = {} else - local to = math.min(line + n, #self.doc.lines) - for i=#self.doc.lines+n,to,-1 do - highlighter_cache[self][i] = highlighter_cache[self][i - n] - end - for i=line,to do - highlighter_cache[self][i] = nil + local blanks = { } + for i = 1, n do + blanks[i] = false end + common.splice(highlighter_cache[self], line, 0, blanks) end end @@ -264,10 +252,7 @@ function Highlighter:remove_notify(line, n, ...) if not highlighter_cache[self] then highlighter_cache[self] = {} else - local to = math.max(line + n, #self.doc.lines) - for i=line,to do - highlighter_cache[self][i] = highlighter_cache[self][i + n] - end + common.splice(highlighter_cache[self], line, n) end end @@ -279,7 +264,7 @@ function Highlighter:tokenize_line(idx, state, ...) if not highlighter_cache[self] then highlighter_cache[self] = {} end - highlighter_cache[self][idx] = nil + highlighter_cache[self][idx] = false return res end @@ -305,14 +290,50 @@ end local MiniMap = Scrollbar:extend() -function MiniMap:new(dv) - MiniMap.super.new(self, { direction = "v", alignment = "e" }) +function MiniMap:new(dv, original_v_scrollbar) + MiniMap.super.new(self, { direction = "v", alignment = "e", + force_status = "expanded", + expanded_size = cached_settings.width, + expanded_margin = 0 }) + self.original_force_status = original_v_scrollbar.force_status + self.original_expanded_size = original_v_scrollbar.expanded_size + self.original_expanded_margin = original_v_scrollbar.expanded_margin self.dv = dv self.enabled = nil + self.was_enabled = true end -function MiniMap:line_highlight_color(line_index) +function MiniMap:swap_to_status() + local enabled = self:is_minimap_enabled() + if not enabled and self.was_enabled then + self.force_status = self.original_force_status + self.expanded_size = self.original_expanded_size + self.expanded_margin = self.original_expanded_margin + self.was_enabled = false + elseif enabled and not self.was_enabled then + self.force_status = "expanded" + self.expanded_size = cached_settings.width + self.expanded_margin = 0 + self.was_enabled = true + end +end + + +function MiniMap:update() + self:swap_to_status() + if self:is_minimap_enabled() then + reset_cache_if_needed() + self.expanded_size = cached_settings.width + local lh = self.dv:get_line_height() + local nlines = self.dv.size.y / lh + self.minimum_thumb_size = nlines * line_spacing + end + MiniMap.super.update(self) +end + + +function MiniMap:line_highlight_color(line_index, docview) -- other plugins can override this, and return a color end @@ -335,97 +356,57 @@ function MiniMap:is_minimap_enabled() end -function MiniMap:get_minimap_dimensions() - local x, y, w, h = self:get_track_rect() - local _, cy, _, cy2 = self.dv:get_content_bounds() - local lh = self.dv:get_line_height() - - local visible_lines_start = math.max(1, math.floor(cy / lh)) - local visible_lines_count = math.max(1, (cy2 - cy) / lh) - local minimap_lines_start = 1 - local minimap_lines_count = math.floor(h / line_spacing) - local line_count = #self.dv.doc.lines - - local is_file_too_large = line_count > 1 and line_count > minimap_lines_count - if is_file_too_large then - local scroll_pos = (visible_lines_start - 1) / - (line_count - visible_lines_count - 1) - scroll_pos = math.min(1.0, scroll_pos) -- 0..1, procent of visual area scrolled - - local thumb_height = visible_lines_count * line_spacing - local scroll_pos_pixels = scroll_pos * (h - thumb_height) - - minimap_lines_start = visible_lines_start - - math.floor(scroll_pos_pixels / line_spacing) - minimap_lines_start = math.max(1, minimap_lines_start) +function MiniMap:_on_mouse_pressed_normal(button, x, y, clicks) + local overlaps = self:_overlaps_normal(x, y) + local percent = MiniMap.super._on_mouse_pressed_normal(self, button, x, y, clicks) + if overlaps == "track" then + -- We need to adjust the percentage to scroll to the line in the minimap + -- that was "clicked" + local minimap_line, _ = self:get_minimap_lines() + local _, track_y, _, _ = self:_get_track_rect_normal() + local line = minimap_line + (y - track_y) // line_spacing + local _, y = self.dv:get_line_screen_position(line) + local _, oy = self.dv:get_content_offset() + local nr = self.normal_rect + percent = common.clamp((y - oy - (self.dv.size.y) / 2) / (nr.scrollable - self.dv.size.y), 0, 1) end - return visible_lines_start, visible_lines_count, minimap_lines_start, minimap_lines_count, is_file_too_large + return percent end -function MiniMap:_get_track_rect_normal() - if not self:is_minimap_enabled() then return MiniMap.super._get_track_rect_normal(self) end - return self.dv.size.x + self.dv.position.x - config.plugins.minimap.width, self.dv.position.y, config.plugins.minimap.width, self.dv.size.y +local function get_visible_minline(dv) + local _, y, _, _ = dv:get_content_bounds() + local lh = dv:get_line_height() + local minline = math.max(0, y / lh + 1) + return minline end -function MiniMap:get_active_margin() if self:is_minimap_enabled() then return 0 else return MiniMap.super.get_active_margin(self) end end +function MiniMap:get_minimap_lines() + local _, track_y, _, h = self:_get_track_rect_normal() + local _, thumb_y, _, _ = self:_get_thumb_rect_normal() + local nlines = h // line_spacing -function MiniMap:_get_thumb_rect_normal() - if not self:is_minimap_enabled() then return MiniMap.super._get_thumb_rect_normal(self) end - local visible_lines_start, visible_lines_count, minimap_lines_start, minimap_lines_count, is_file_too_large = self:get_minimap_dimensions() - local visible_y = self.dv.position.y + (visible_lines_start - 1) * line_spacing - if is_file_too_large then - local line_count = #self.dv.doc.lines - local scroll_pos = (visible_lines_start - 1) / - (line_count - visible_lines_count - 1) - scroll_pos = math.min(1.0, scroll_pos) -- 0..1, procent of visual area scrolled - - local thumb_height = visible_lines_count * line_spacing - local scroll_pos_pixels = scroll_pos * (self.dv.size.y - thumb_height) - visible_y = self.dv.position.y + scroll_pos_pixels + local minline = get_visible_minline(self.dv) + local top_lines = (thumb_y - track_y) / line_spacing + local lines_start, offset = math.modf(minline - top_lines) + if lines_start <= 1 and nlines >= #self.dv.doc.lines then + offset = 0 end - return self.dv.size.x + self.dv.position.x - config.plugins.minimap.width, visible_y, config.plugins.minimap.width, visible_lines_count * line_spacing + return common.clamp(lines_start, 1, #self.dv.doc.lines), common.clamp(nlines, 1, #self.dv.doc.lines), offset * line_spacing end -function MiniMap:on_mouse_pressed(button, x, y, clicks) - local percent = MiniMap.super.on_mouse_pressed(self, button, x, y, clicks) - if not self:is_minimap_enabled() or not percent then return percent end - local _, visible_lines_count, minimap_lines_start, minimap_lines_count, is_file_too_large = self:get_minimap_dimensions() - local _, _, w, h = self:get_track_rect() - local tx, ty, tw, th = self:get_thumb_rect() - if y >= ty and y < ty + th then self.drag_start_offset = (y - ty) - th / 2 return self.percent end - self.drag_start_offset = 0 - self.hovering.thumb = x >= tx and x < tx + tw and y >= ty and y < ty + th - self.dragging = self.hovering.thumb - local lh = self.dv:get_line_height() - percent = math.max(0.0, math.min((y - self.dv.position.y) / h, 1.0)) - return (((percent * minimap_lines_count) + minimap_lines_start) * lh / self.dv:get_scrollable_size()) - (visible_lines_count / #self.dv.doc.lines / 2) +function MiniMap:set_size(x, y, w, h, scrollable) + if not self:is_minimap_enabled() then return MiniMap.super.set_size(self, x, y, w, h, scrollable) end + -- If possible, use the size needed to only manage the visible minimap lines. + -- This allows us to let Scrollbar manage the thumb. + h = math.min(h, line_spacing * (scrollable // self.dv:get_line_height())) + MiniMap.super.set_size(self, x, y, w, h, scrollable) end -function MiniMap:on_mouse_moved(x, y, dx, dy) - local percent = MiniMap.super.on_mouse_moved(self, x, y, dx, dy) - if not self:is_minimap_enabled() or type(percent) ~= "number" then return percent end - local _, visible_lines_count, minimap_lines_start, minimap_lines_count, is_file_too_large = self:get_minimap_dimensions() - local lh = self.dv:get_line_height() - local _, _, w, h = self:get_track_rect() - local tx, ty, tw, th = self:get_thumb_rect() - if x >= tx and x < tx + tw and y >= ty and y < ty + th then self.hovering.thumb = true end - if not self.hovering.thumb then return self.percent end - y = y - self.drag_start_offset - percent = math.max(0.0, math.min((y - self.dv.position.y) / h, 1.0)) - return (((percent * minimap_lines_count) + minimap_lines_start) * lh / self.dv:get_scrollable_size()) - (visible_lines_count / #self.dv.doc.lines / 2) -end - -function MiniMap:draw_thumb() - local color = self.hovering.thumb and style.scrollbar2 or style.scrollbar - local x, y, w, h = self:get_thumb_rect() - renderer.draw_rect(x, y, w, h, color) -end - function MiniMap:draw() if not self:is_minimap_enabled() then return MiniMap.super.draw(self) end local dv = self.dv @@ -434,25 +415,27 @@ function MiniMap:draw() local highlight = dv.hovered_scrollbar or dv.dragging_scrollbar local visual_color = highlight and style.scrollbar2 or style.scrollbar - local visible_lines_start, visible_lines_count, - minimap_lines_start, minimap_lines_count = self:get_minimap_dimensions() if config.plugins.minimap.draw_background then - renderer.draw_rect(x, y, w, h, style.minimap_background or style.background) + renderer.draw_rect(x, y, w, self.dv.size.y, style.minimap_background or style.background) end self:draw_thumb() + local minimap_lines_start, minimap_lines_count, y_offset = self:get_minimap_lines() + local line_selection_offset = line_spacing - char_height + y = y - y_offset + line_selection_offset + -- highlight the selected lines, and the line with the caret on it local selection_color = config.plugins.minimap.selection_color or style.dim local caret_color = config.plugins.minimap.caret_color or style.caret - for i, line1, col1, line2, col2 in dv.doc:get_selections() do - local selection1_y = y + (line1 - minimap_lines_start) * line_spacing - local selection2_y = y + (line2 - minimap_lines_start) * line_spacing + for _, line1, _, line2, _ in dv.doc:get_selections() do + local selection1_y = y + (line1 - minimap_lines_start) * line_spacing - line_selection_offset + local selection2_y = y + (line2 - minimap_lines_start) * line_spacing - line_selection_offset local selection_min_y = math.min(selection1_y, selection2_y) - local selection_h = math.abs(selection2_y - selection1_y)+1 + local selection_h = math.abs(selection2_y - selection1_y) + 1 + line_selection_offset renderer.draw_rect(x, selection_min_y, w, selection_h, selection_color) - renderer.draw_rect(x, selection1_y, w, line_spacing, caret_color) + renderer.draw_rect(x, selection1_y, w, line_spacing + line_selection_offset, caret_color) end local highlight_align = config.plugins.minimap.highlight_align @@ -519,16 +502,15 @@ function MiniMap:draw() highlight_x = x + w - highlight_width end local function render_highlight(idx, line_y) - local highlight_color = self:line_highlight_color(idx) + local highlight_color = self:line_highlight_color(idx, self.dv) if highlight_color then - renderer.draw_rect(highlight_x, line_y, highlight_width, line_spacing, highlight_color) + renderer.draw_rect(highlight_x, line_y - line_selection_offset, + highlight_width, line_spacing + line_selection_offset, highlight_color) end end local endidx = math.min(minimap_lines_start + minimap_lines_count, #self.dv.doc.lines) - reset_cache_if_needed() - if not highlighter_cache[dv.doc.highlighter] then highlighter_cache[dv.doc.highlighter] = {} end @@ -603,23 +585,9 @@ end local old_docview_new = DocView.new function DocView:new(doc) old_docview_new(self, doc) - if self:is(DocView) then self.v_scrollbar = MiniMap(self) end -end - -local old_docview_scroll_to_make_visible = DocView.scroll_to_make_visible -function DocView:scroll_to_make_visible(line, col, ...) - if - not self:is(DocView) or not self.v_scrollbar:is(MiniMap) - or - not self.v_scrollbar:is_minimap_enabled() - then - return old_docview_scroll_to_make_visible(self, line, col, ...) + if self:is(DocView) then + self.v_scrollbar = MiniMap(self, self.v_scrollbar) end - local old_size = self.size.x - self.size.x = math.max(0, self.size.x - config.plugins.minimap.width) - local result = old_docview_scroll_to_make_visible(self, line, col, ...) - self.size.x = old_size - return result end @@ -663,4 +631,3 @@ command.add("core.docview!", { }) return MiniMap - diff --git a/resources/amiga/addons/plugins/sort.lua b/resources/amiga/addons/plugins/sort.lua new file mode 100644 index 00000000..b933bb4c --- /dev/null +++ b/resources/amiga/addons/plugins/sort.lua @@ -0,0 +1,31 @@ +-- mod-version:3 +local core = require "core" +local command = require "core.command" +local translate = require "core.doc.translate" + +local function split_lines(text) + local res = {} + for line in (text .. "\n"):gmatch("(.-)\n") do + table.insert(res, line) + end + return res +end + +command.add("core.docview!", { + ["sort:sort"] = function(dv) + local doc = dv.doc + + local l1, c1, l2, c2, swap = doc:get_selection(true) + l1, c1 = translate.start_of_line(doc, l1, c1) + l2, c2 = translate.end_of_line(doc, l2, c2) + doc:set_selection(l1, c1, l2, c2, swap) + + doc:replace(function(text) + local head, body, foot = text:match("(\n*)(.-)(\n*)$") + local lines = split_lines(body) + table.sort(lines, function(a, b) return a:lower() < b:lower() end) + return head .. table.concat(lines, "\n") .. foot, 1 + end) + end, +}) + diff --git a/resources/amiga/addons/plugins/sortcss.lua b/resources/amiga/addons/plugins/sortcss.lua new file mode 100644 index 00000000..9e360221 --- /dev/null +++ b/resources/amiga/addons/plugins/sortcss.lua @@ -0,0 +1,413 @@ +-- mod-version:3 + +--[[ + Lite-XL plugin to sort CSS properties alphabetically or using + the concentric model by https://rhodesmill.org/brandon/2011/concentric-css/ + + This plugin is based on vscode plugin: https://github.com/roubaobaozi/vscode-sort-selection-concentrically + + Usage: + 1. Select CSS code (must have one property per line). + 2. Press control+alt+a to sort alphabetically or control+alt+c to sort concentrically. + Alternatively you can also right-click and select the appropriate option. +--]] + +local core = require "core" +local config = require "core.config" +local common = require "core.common" +local command = require "core.command" +local keymap = require "core.keymap" +local contextmenu = require "plugins.contextmenu" + +local css_syntaxes = { + "CSS", + "HTML", + "JSX", + "TypeScript with JSX", +} + +local concentric_order = { + -- browser default styles + "all", + "appearance", + + -- box model + "box-sizing", + + -- position + "display", + "position", + "top", + "right", + "bottom", + "left", + + "float", + "clear", + + -- flex + "flex", + "flex-basis", + "flex-direction", + "flex-flow", + "flex-grow", + "flex-shrink", + "flex-wrap", + + -- grid + "grid", + "grid-area", + "grid-template", + "grid-template-areas", + "grid-template-rows", + "grid-template-columns", + "grid-row", + "grid-row-start", + "grid-row-end", + "grid-column", + "grid-column-start", + "grid-column-end", + "grid-auto-rows", + "grid-auto-columns", + "grid-auto-flow", + "grid-gap", + "grid-row-gap", + "grid-column-gap", + + -- flex align + "align-content", + "align-items", + "align-self", + + -- flex justify + "justify-content", + "justify-items", + "justify-self", + + -- order + "order", + + -- columns + "columns", + "column-gap", + "column-fill", + "column-rule", + "column-rule-width", + "column-rule-style", + "column-rule-color", + "column-span", + "column-count", + "column-width", + + -- transform + "backface-visibility", + "perspective", + "perspective-origin", + "transform", + "transform-origin", + "transform-style", + + -- transitions + "transition", + "transition-delay", + "transition-duration", + "transition-property", + "transition-timing-function", + + -- visibility + "visibility", + "opacity", + "mix-blend-mode", + "isolation", + "z-index", + + -- margin + "margin", + "margin-top", + "margin-right", + "margin-bottom", + "margin-left", + + -- outline + "outline", + "outline-offset", + "outline-width", + "outline-style", + "outline-color", + + -- border + "border", + "border-top", + "border-right", + "border-bottom", + "border-left", + "border-width", + "border-top-width", + "border-right-width", + "border-bottom-width", + "border-left-width", + + -- border-style + "border-style", + "border-top-style", + "border-right-style", + "border-bottom-style", + "border-left-style", + + -- border-radius + "border-radius", + "border-top-left-radius", + "border-top-right-radius", + "border-bottom-left-radius", + "border-bottom-right-radius", + + -- border-color + "border-color", + "border-top-color", + "border-right-color", + "border-bottom-color", + "border-left-color", + + -- border-image + "border-image", + "border-image-source", + "border-image-width", + "border-image-outset", + "border-image-repeat", + "border-image-slice", + + -- box-shadow + "box-shadow", + + -- background + "background", + "background-attachment", + "background-clip", + "background-color", + "background-image", + "background-origin", + "background-position", + "background-repeat", + "background-size", + "background-blend-mode", + + -- cursor + "cursor", + + -- padding + "padding", + "padding-top", + "padding-right", + "padding-bottom", + "padding-left", + + -- width + "width", + "min-width", + "max-width", + + -- height + "height", + "min-height", + "max-height", + + -- overflow + "overflow", + "overflow-x", + "overflow-y", + "resize", + + -- list-style + "list-style", + "list-style-type", + "list-style-position", + "list-style-image", + "caption-side", + + -- tables + "table-layout", + "border-collapse", + "border-spacing", + "empty-cells", + + -- animation + "animation", + "animation-name", + "animation-duration", + "animation-timing-function", + "animation-delay", + "animation-iteration-count", + "animation-direction", + "animation-fill-mode", + "animation-play-state", + + -- vertical-alignment + "vertical-align", + + -- text-alignment & decoration + "direction", + "tab-size", + "text-align", + "text-align-last", + "text-justify", + "text-indent", + "text-transform", + "text-decoration", + "text-decoration-color", + "text-decoration-line", + "text-decoration-style", + "text-rendering", + "text-shadow", + "text-overflow", + + -- text-spacing + "line-height", + "word-spacing", + "letter-spacing", + "white-space", + "word-break", + "word-wrap", + "color", + + -- font + "font", + "font-family", + "font-kerning", + "font-size", + "font-size-adjust", + "font-stretch", + "font-weight", + "font-smoothing", + "osx-font-smoothing", + "font-variant", + "font-style", + + -- content + "content", + "quotes", + + -- counters + "counter-reset", + "counter-increment", + + -- breaks + "page-break-before", + "page-break-after", + "page-break-inside", + + -- mouse + "pointer-events", + + -- intent + "will-change" +} + +config.plugins.sortcss = common.merge({ + css_syntaxes = css_syntaxes, + concentric_order = concentric_order, + -- The config specification used by the settings gui + config_spec = { + name = "Sort CSS", + { + label = "CSS file syntaxes", + description = "List of CSS-compatible syntax names.", + path = "css_syntaxes", + type = "list_strings", + default = css_syntaxes + }, + } +}, config.plugins.sortcss) + +local function compare_alphabetical(line1, line2) + local prop1 = line1:match("^%s*([^:]+)") + local prop2 = line2:match("^%s*([^:]+)") + + return string.lower(prop1) < string.lower(prop2) +end + +local function compare_concentrical(line1, line2) + local prop1 = line1:match("^%s*([^:]+)") + local prop2 = line2:match("^%s*([^:]+)") + + local index1 = 0 + local index2 = 0 + + for i, prop in ipairs(config.plugins.sortcss.concentric_order) do + if prop == prop1 then + index1 = i + end + if prop == prop2 then + index2 = i + end + end + + if index1 == 0 then + index1 = #concentric_order + 1 + end + + if index2 == 0 then + index2 = #concentric_order + 1 + end + + return index1 < index2 +end + +local function sort_css(str, order) + local lines = {} + for line in str:gmatch("[^\r\n]+") do + table.insert(lines, line) + end + + if order == "alphabetical" then + table.sort(lines, compare_alphabetical) + end + + if order == "concentrical" then + table.sort(lines, compare_concentrical) + end + + return table.concat(lines, "\n") +end + +command.add("core.docview", { + ["sortcss:alphabetical"] = function(dv) + local doc = dv.doc + if not doc:has_selection() then + core.error("No text selected") + return + end + + local text = doc:get_text(doc:get_selection()) + doc:text_input(sort_css(text, "alphabetical")) + end, + ["sortcss:concentrical"] = function(dv) + local doc = dv.doc + if not doc:has_selection() then + core.error("No text selected") + return + end + + local text = doc:get_text(doc:get_selection()) + doc:text_input(sort_css(text, "concentrical")) + end, +}) + +contextmenu:register(function() + local doc = core.active_view.doc + if doc and doc:has_selection() then + for _, v in pairs(config.plugins.sortcss.css_syntaxes) do + if v == doc.syntax.name then + return true, core.active_view + end + end + end + + return false +end, { + contextmenu.DIVIDER, + { text = "Sort CSS Selection Alphabetically", command = "sortcss:alphabetical" }, + { text = "Sort CSS Selection Concentrically", command = "sortcss:concentrical" }, +}) + +keymap.add { ["ctrl+alt+a"] = "sortcss:alphabetical" } +keymap.add { ["ctrl+alt+c"] = "sortcss:concentrical" } diff --git a/resources/amiga/addons/plugins/todotreeview-xl.lua b/resources/amiga/addons/plugins/todotreeview-xl.lua new file mode 100644 index 00000000..8703b7db --- /dev/null +++ b/resources/amiga/addons/plugins/todotreeview-xl.lua @@ -0,0 +1,820 @@ +-- mod-version:3 +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 CommandView = require "core.commandview" +local DocView = require "core.docview" + +local TodoTreeView = View:extend() + +local SCOPES = { + ALL = "all", + FOCUSED = "focused", +} + +config.plugins.todotreeview = common.merge({ + todo_tags = {"TODO", "BUG", "FIX", "FIXME", "IMPROVEMENT"}, + 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}, + }, + todo_file_color = { + name=style.text, + hover=style.accent + }, + -- Paths or files to be ignored + ignore_paths = {}, + + -- Tells if the plugin should start with the nodes expanded + 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 + todo_mode = "tag", + + treeview_size = 200 * SCALE, -- default size + + -- Only used in file mode when the tag and the text are on the same line + todo_separator = " - ", + + -- Text displayed when the note is empty + todo_default_text = "blank", + + -- 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 + todo_scope = SCOPES.ALL, + + -- The config specification used by the settings gui + config_spec = { + name = "TodoTreeView", + { + label = "Todo Tags", + description = "List of tags to parse and show in the todo panel.", + path = "todo_tags", + type = "list_strings", + default = {"TODO", "BUG", "FIX", "FIXME", "IMPROVEMENT"}, + }, + { + label = "Paths to ignore", + description = "Paths to be ignored when parsing the tags.", + path = "ignore_paths", + type = "list_strings", + default = {} + }, + { + label = "Groups Expanded", + description = "Defines if the groups (Tags / Files) should start expanded.", + path = "todo_expanded", + type = "toggle", + default = true, + }, + { + label = "Mode for the todos", + description = "Mode for the todos to be displayed.", + path = "todo_mode", + type = "selection", + default = "tag", + values = { + {"Tag", "tag"}, + {"File", "file"}, + {"FileTag", "file_tag"} + } + }, + { + label = "Treeview Size", + description = "Size of the todo tree view panel.", + path = "treeview_size", + type = "number", + default = 200 * SCALE, + }, + { + label = "Todo separator", + description = "Separator used in file mode when the tag and the text are on the same line.", + path = "todo_separator", + type = "string", + default = " - ", + }, + { + label = "Empty Default Text", + description = "Default text displayed for a note when it has no text.", + path = "todo_default_text", + type = "string", + default = " - ", + }, + { + label = "Todo Scope", + description = "Scope for the notes to be picked, all notes or currently focused file.", + path = "todo_scope", + type = "selection", + default = "all", + values = { + {"All", "all"}, + {"Focused", "focused"}, + } + }, + } +}, config.plugins.todotreeview) + +local icon_small_font = style.icon_font:copy(10 * SCALE) + +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.plugins.todotreeview.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.plugins.todotreeview.todo_scope == SCOPES.ALL then + return true + elseif config.plugins.todotreeview.todo_scope == SCOPES.FOCUSED then + if core.active_view:is(CommandView) or core.active_view:is(TodoTreeView) then + if self.previous_focused_file then + return self.previous_focused_file == filename + end + elseif core.active_view:is(DocView) then + return core.active_view.doc.filename == filename + end + return true + else + assert(false, "Unknown scope defined ("..config.plugins.todotreeview.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.plugins.todotreeview.todo_mode == "file" then + items[cached.filename] = cached + elseif config.plugins.todotreeview.todo_mode == "file_tag" then + local file_t = {} + file_t.expanded = config.plugins.todotreeview.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.plugins.todotreeview.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.plugins.todotreeview.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.plugins.todotreeview.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.plugins.todotreeview.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.plugins.todotreeview.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.plugins.todotreeview.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:set_target_size(axis, value) + if axis == "x" then + config.plugins.todotreeview.treeview_size = value + return true + end +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 filename, 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 + self.previous_focused_file = core.active_view.doc.filename + elseif core.active_view:is(CommandView) or core.active_view:is(TodoTreeView) then + -- Do nothing + 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.plugins.todotreeview.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.plugins.todotreeview.todo_file_color.name or style.text + if config.plugins.todotreeview.tag_colors[item.tag] then + text_color = config.plugins.todotreeview.tag_colors[item.tag].text or style.text + tag_color = config.plugins.todotreeview.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.plugins.todotreeview.todo_file_color.hover or style.accent + if config.plugins.todotreeview.tag_colors[item.tag] then + text_color = config.plugins.todotreeview.tag_colors[item.tag].text_hover or style.accent + tag_color = config.plugins.todotreeview.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.plugins.todotreeview.todo_mode == "file_tag" then + x = x + style.padding.x * 0.75 + end + + if item.expanded then + common.draw_text(style.icon_font, tag_color, "-", nil, x, y, 0, h) + else + common.draw_text(icon_small_font, tag_color, ">", nil, x, y, 0, h) + end + x = x + icon_width / 2 + else + if config.plugins.todotreeview.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.plugins.todotreeview.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.plugins.todotreeview.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.plugins.todotreeview.treeview_size +node:split("right", view, {x=true}, true) + +core.status_view:add_item({ + predicate = function() + return #view.filter > 0 and core.active_view and not core.active_view:is(CommandView) + end, + name = "todotreeview:filter", + alignment = core.status_view.Item.RIGHT, + get_item = function() + return { + style.text, + string.format("Filter: %s", view.filter) + } + end, + position = 1, + tooltip = "Todos filtered by", + separator = core.status_view.separator2 +}) + +-- 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 + 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", { + text = view.filter, + submit = submit, + suggest = suggest, + cancel = 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.plugins.todotreeview.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" } +