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:
**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

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({
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

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",
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
if self:is(DocView) then
self.v_scrollbar = MiniMap(self, self.v_scrollbar)
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
@ -663,4 +631,3 @@ command.add("core.docview!", {
})
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" }