Added and updated many plugins

This commit is contained in:
George Sokianos 2024-12-26 20:43:53 +00:00
parent 782a7acc21
commit 0ebc0e789d
9 changed files with 1695 additions and 164 deletions

View File

@ -70,6 +70,10 @@ need.
The included plugins are the following: 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** **autoinsert**
Automatically inserts closing brackets and quotes. Also allows selected Automatically inserts closing brackets and quotes. Also allows selected
text to be wrapped with brackets or quotes. text to be wrapped with brackets or quotes.
@ -124,6 +128,9 @@ folder like below:
**indentguide** **indentguide**
Adds indent guides Adds indent guides
**keyhud**
Simple key HUD display for lite-xl.
**language_guide** **language_guide**
Syntax for the AmigaGuide scripting language Syntax for the AmigaGuide scripting language
@ -142,6 +149,9 @@ Automatically inserts indentation and closing bracket/text after newline
**markers** **markers**
Add markers to docs and jump between them quickly Add markers to docs and jump between them quickly
**memoryusage**
Show memory usage in the status view
**minimap** **minimap**
Shows a minimap on the right-hand side of the docview. Please note that 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. 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** **smallclock**
It adds a small clock at the bottom right corner. 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** **tetris**
Play Tetris inside Lite XL. Play Tetris inside Lite XL.
**todotreeview**
Todo tree viewer for annotations in code like TODO, BUG, FIX,
IMPROVEMENT
## Tips and tricks ## Tips and tricks
### Transitions ### Transitions
@ -231,12 +251,21 @@ https://git.walkero.gr/walkero/lite-xl/issues
# Changelog # Changelog
## [2.1.7r1] - 2024-12-26 ## [2.1.7r1] - 2024-12-26
### Added ### Added
- Added widget library - Added the widget library
- Added settings plugin that shows a GUI for chnaging the app settings - Added the settings plugin that shows a GUI for chnaging the app settings
- Added search_ui plugin that adds a GUI for search - 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
- Updated the code to the upstream 2.1.7 release - Updated the code to the upstream 2.1.7 release
- Updated the minimap plugin
- Updated the indentguide plugin
## [2.1.6r1] - 2024-12-3 ## [2.1.6r1] - 2024-12-3
### Changed ### Changed

View File

@ -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

View File

@ -6,6 +6,7 @@ local DocView = require "core.docview"
config.plugins.indentguide = common.merge({ config.plugins.indentguide = common.merge({
enabled = true, enabled = true,
highlight = true,
-- The config specification used by the settings gui -- The config specification used by the settings gui
config_spec = { config_spec = {
name = "Indent Guide", name = "Indent Guide",
@ -15,28 +16,28 @@ config.plugins.indentguide = common.merge({
path = "enabled", path = "enabled",
type = "toggle", type = "toggle",
default = true default = true
},
{
label = "Highlight Line",
description = "Toggle the highlight of the curent indentation indicator lines.",
path = "highlight",
type = "toggle",
default = true
} }
} }
}, config.plugins.indentguide) }, config.plugins.indentguide)
-- TODO: replace with `doc:get_indent_info()` when 2.1 releases local indentguide = {}
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
function indentguide.get_line_spaces(doc, line, dir)
local function get_line_spaces(doc, line, dir) local _, indent_size = doc:get_indent_info()
local _, indent_size = get_indent_info(doc)
local text = doc.lines[line] local text = doc.lines[line]
if not text or #text == 1 then if not text or #text == 1 then
return -1 return -1
end end
local s, e = text:find("^%s*") local s, e = text:find("^%s*")
if e == #text then if e == #text then
return get_line_spaces(doc, line + dir, dir) return indentguide.get_line_spaces(doc, line + dir, dir)
end end
local n = 0 local n = 0
for _,b in pairs({text:byte(s, e)}) do for _,b in pairs({text:byte(s, e)}) do
@ -46,15 +47,16 @@ local function get_line_spaces(doc, line, dir)
end 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 if doc.lines[line]:find("^%s*\n") then
return math.max( return math.max(
get_line_spaces(doc, line - 1, -1), indentguide.get_line_spaces(doc, line - 1, -1),
get_line_spaces(doc, line + 1, 1)) indentguide.get_line_spaces(doc, line + 1, 1))
end end
return get_line_spaces(doc, line) return indentguide.get_line_spaces(doc, line)
end end
local docview_update = DocView.update local docview_update = DocView.update
function DocView:update() function DocView:update()
docview_update(self) docview_update(self)
@ -66,7 +68,7 @@ function DocView:update()
local function get_indent(line) local function get_indent(line)
if line < 1 or line > #self.doc.lines then return -1 end if line < 1 or line > #self.doc.lines then return -1 end
if not self.indentguide_indents[line] then 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 end
return self.indentguide_indents[line] return self.indentguide_indents[line]
end end
@ -76,10 +78,10 @@ function DocView:update()
local minline, maxline = self:get_visible_line_range() local minline, maxline = self:get_visible_line_range()
for i = minline, maxline do 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 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 for _,line in self.doc:get_selections() do
local lvl = get_indent(line) local lvl = get_indent(line)
local top, bottom local top, bottom
@ -121,19 +123,26 @@ function DocView:update()
end end
function indentguide.get_width()
return math.max(1, SCALE)
end
local draw_line_text = DocView.draw_line_text local draw_line_text = DocView.draw_line_text
function DocView:draw_line_text(line, x, y) function DocView:draw_line_text(line, x, y)
if config.plugins.indentguide.enabled and self:is(DocView) then if config.plugins.indentguide.enabled and self:is(DocView) then
local spaces = self.indentguide_indents[line] or -1 local spaces = self.indentguide_indents[line] or -1
local _, indent_size = get_indent_info(self.doc) local _, indent_size = self.doc:get_indent_info()
local w = math.max(1, SCALE) local w = indentguide.get_width()
local h = self:get_line_height() local h = self:get_line_height()
local font = self:get_font() local font = self:get_font()
local space_sz = font:get_width(" ") local space_sz = font:get_width(" ")
for i = 0, spaces - 1, indent_size do for i = 0, spaces - 1, indent_size do
local color = style.guide or style.selection local color = style.guide or style.selection
local active_lvl = self.indentguide_indent_active[line] or -1 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 color = style.guide_highlight or style.accent
end end
local sw = space_sz * i local sw = space_sz * i
@ -142,3 +151,5 @@ function DocView:draw_line_text(line, x, y)
end end
return draw_line_text(self, line, x, y) return draw_line_text(self, line, x, y)
end end
return indentguide

View File

@ -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"] = "<ESC>",
["space"] = "<SPACE>", --"␣""
["left gui"] = "<CMD>", --"⌘"
["right gui"] = "<CMD>",
["left ctrl"] = "<CTRL>",
["control"] = "<CTRL>",
["right ctrl"] = "<CTRL>",
["left alt"] = "<ALT>",
["right alt"] = "<ALT>",
["left amiga"] = "<LAMIGA>",
["right amiga"] = "<RAMIGA>",
["left"] = "",
["right"] = "",
["up"] = "",
["down"] = "",
["left shift"] = "",
["right shift"] = "",
["capslock"] = "",
["return"] = "<RETURN>", --"↵",
["backspace"] = "",
["delete"] = "",
["pageup"] = "<UP>", --"⇞",
["pagedown"] = "<DOWN>", --"⇟",
["home"] = "<HOME>", --"↖",
["end"] = "<END>", --"↘",
["tab"] = "<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

View File

@ -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
})

View File

@ -135,29 +135,21 @@ config.plugins.minimap = common.merge({
}, },
{ {
label = "Selection Color", label = "Selection Color",
description = "Background color of selected text in html notation eg: #FFFFFF. Leave empty to use default.", description = "Background color of selected text.",
path = "selection_color_html", path = "selection_color",
type = "string", type = "color",
on_apply = function(value) default = string.format("#%02X%02X%02X%02X",
if value and value:match("#%x%x%x%x%x%x") then style.dim[1], style.dim[2], style.dim[3], style.dim[4]
config.plugins.minimap.selection_color = { common.color(value) } )
else
config.plugins.minimap.selection_color = nil
end
end
}, },
{ {
label = "Caret Color", label = "Caret Color",
description = "Background color of active line in html notation eg: #FFFFFF. Leave empty to use default.", description = "Background color of active line.",
path = "caret_color_html", path = "caret_color",
type = "string", type = "color",
on_apply = function(value) default = string.format("#%02X%02X%02X%02X",
if value and value:match("#%x%x%x%x%x%x") then style.caret[1], style.caret[2], style.caret[3], style.caret[4]
config.plugins.minimap.caret_color = { common.color(value) } )
else
config.plugins.minimap.caret_color = nil
end
end
}, },
{ {
label = "Highlight Alignment", label = "Highlight Alignment",
@ -236,8 +228,6 @@ local function reset_cache_if_needed()
end end
-- Move cache to make space for new lines -- Move cache to make space for new lines
local prev_insert_notify = Highlighter.insert_notify local prev_insert_notify = Highlighter.insert_notify
function Highlighter:insert_notify(line, n, ...) function Highlighter:insert_notify(line, n, ...)
@ -246,13 +236,11 @@ function Highlighter:insert_notify(line, n, ...)
if not highlighter_cache[self] then if not highlighter_cache[self] then
highlighter_cache[self] = {} highlighter_cache[self] = {}
else else
local to = math.min(line + n, #self.doc.lines) local blanks = { }
for i=#self.doc.lines+n,to,-1 do for i = 1, n do
highlighter_cache[self][i] = highlighter_cache[self][i - n] blanks[i] = false
end
for i=line,to do
highlighter_cache[self][i] = nil
end end
common.splice(highlighter_cache[self], line, 0, blanks)
end end
end end
@ -264,10 +252,7 @@ function Highlighter:remove_notify(line, n, ...)
if not highlighter_cache[self] then if not highlighter_cache[self] then
highlighter_cache[self] = {} highlighter_cache[self] = {}
else else
local to = math.max(line + n, #self.doc.lines) common.splice(highlighter_cache[self], line, n)
for i=line,to do
highlighter_cache[self][i] = highlighter_cache[self][i + n]
end
end end
end end
@ -279,7 +264,7 @@ function Highlighter:tokenize_line(idx, state, ...)
if not highlighter_cache[self] then if not highlighter_cache[self] then
highlighter_cache[self] = {} highlighter_cache[self] = {}
end end
highlighter_cache[self][idx] = nil highlighter_cache[self][idx] = false
return res return res
end end
@ -305,14 +290,50 @@ end
local MiniMap = Scrollbar:extend() local MiniMap = Scrollbar:extend()
function MiniMap:new(dv) function MiniMap:new(dv, original_v_scrollbar)
MiniMap.super.new(self, { direction = "v", alignment = "e" }) 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.dv = dv
self.enabled = nil self.enabled = nil
self.was_enabled = true
end 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 -- other plugins can override this, and return a color
end end
@ -335,97 +356,57 @@ function MiniMap:is_minimap_enabled()
end end
function MiniMap:get_minimap_dimensions() function MiniMap:_on_mouse_pressed_normal(button, x, y, clicks)
local x, y, w, h = self:get_track_rect() local overlaps = self:_overlaps_normal(x, y)
local _, cy, _, cy2 = self.dv:get_content_bounds() local percent = MiniMap.super._on_mouse_pressed_normal(self, button, x, y, clicks)
local lh = self.dv:get_line_height() if overlaps == "track" then
-- We need to adjust the percentage to scroll to the line in the minimap
local visible_lines_start = math.max(1, math.floor(cy / lh)) -- that was "clicked"
local visible_lines_count = math.max(1, (cy2 - cy) / lh) local minimap_line, _ = self:get_minimap_lines()
local minimap_lines_start = 1 local _, track_y, _, _ = self:_get_track_rect_normal()
local minimap_lines_count = math.floor(h / line_spacing) local line = minimap_line + (y - track_y) // line_spacing
local line_count = #self.dv.doc.lines local _, y = self.dv:get_line_screen_position(line)
local _, oy = self.dv:get_content_offset()
local is_file_too_large = line_count > 1 and line_count > minimap_lines_count local nr = self.normal_rect
if is_file_too_large then percent = common.clamp((y - oy - (self.dv.size.y) / 2) / (nr.scrollable - self.dv.size.y), 0, 1)
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)
end end
return visible_lines_start, visible_lines_count, minimap_lines_start, minimap_lines_count, is_file_too_large return percent
end end
function MiniMap:_get_track_rect_normal() local function get_visible_minline(dv)
if not self:is_minimap_enabled() then return MiniMap.super._get_track_rect_normal(self) end local _, y, _, _ = dv:get_content_bounds()
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 lh = dv:get_line_height()
local minline = math.max(0, y / lh + 1)
return minline
end 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() local minline = get_visible_minline(self.dv)
if not self:is_minimap_enabled() then return MiniMap.super._get_thumb_rect_normal(self) end local top_lines = (thumb_y - track_y) / line_spacing
local visible_lines_start, visible_lines_count, minimap_lines_start, minimap_lines_count, is_file_too_large = self:get_minimap_dimensions() local lines_start, offset = math.modf(minline - top_lines)
local visible_y = self.dv.position.y + (visible_lines_start - 1) * line_spacing if lines_start <= 1 and nlines >= #self.dv.doc.lines then
if is_file_too_large then offset = 0
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
end 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 end
function MiniMap:on_mouse_pressed(button, x, y, clicks) function MiniMap:set_size(x, y, w, h, scrollable)
local percent = MiniMap.super.on_mouse_pressed(self, button, x, y, clicks) if not self:is_minimap_enabled() then return MiniMap.super.set_size(self, x, y, w, h, scrollable) end
if not self:is_minimap_enabled() or not percent then return percent end -- If possible, use the size needed to only manage the visible minimap lines.
local _, visible_lines_count, minimap_lines_start, minimap_lines_count, is_file_too_large = self:get_minimap_dimensions() -- This allows us to let Scrollbar manage the thumb.
local _, _, w, h = self:get_track_rect() h = math.min(h, line_spacing * (scrollable // self.dv:get_line_height()))
local tx, ty, tw, th = self:get_thumb_rect() MiniMap.super.set_size(self, x, y, w, h, scrollable)
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)
end 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() function MiniMap:draw()
if not self:is_minimap_enabled() then return MiniMap.super.draw(self) end if not self:is_minimap_enabled() then return MiniMap.super.draw(self) end
local dv = self.dv local dv = self.dv
@ -434,25 +415,27 @@ function MiniMap:draw()
local highlight = dv.hovered_scrollbar or dv.dragging_scrollbar local highlight = dv.hovered_scrollbar or dv.dragging_scrollbar
local visual_color = highlight and style.scrollbar2 or style.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 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 end
self:draw_thumb() 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 -- highlight the selected lines, and the line with the caret on it
local selection_color = config.plugins.minimap.selection_color or style.dim local selection_color = config.plugins.minimap.selection_color or style.dim
local caret_color = config.plugins.minimap.caret_color or style.caret local caret_color = config.plugins.minimap.caret_color or style.caret
for i, line1, col1, line2, col2 in dv.doc:get_selections() do for _, line1, _, line2, _ in dv.doc:get_selections() do
local selection1_y = y + (line1 - minimap_lines_start) * line_spacing local selection1_y = y + (line1 - minimap_lines_start) * line_spacing - line_selection_offset
local selection2_y = y + (line2 - minimap_lines_start) * line_spacing 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_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, 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 end
local highlight_align = config.plugins.minimap.highlight_align local highlight_align = config.plugins.minimap.highlight_align
@ -519,16 +502,15 @@ function MiniMap:draw()
highlight_x = x + w - highlight_width highlight_x = x + w - highlight_width
end end
local function render_highlight(idx, line_y) 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 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
end end
local endidx = math.min(minimap_lines_start + minimap_lines_count, #self.dv.doc.lines) 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 if not highlighter_cache[dv.doc.highlighter] then
highlighter_cache[dv.doc.highlighter] = {} highlighter_cache[dv.doc.highlighter] = {}
end end
@ -603,23 +585,9 @@ end
local old_docview_new = DocView.new local old_docview_new = DocView.new
function DocView:new(doc) function DocView:new(doc)
old_docview_new(self, doc) old_docview_new(self, doc)
if self:is(DocView) then self.v_scrollbar = MiniMap(self) end if self:is(DocView) then
self.v_scrollbar = MiniMap(self, self.v_scrollbar)
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, ...)
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 end
@ -663,4 +631,3 @@ command.add("core.docview!", {
}) })
return MiniMap return MiniMap

View File

@ -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,
})

View File

@ -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" }

View File

@ -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" }