diff options
author | jgmdev <jgmdev@gmail.com> | 2022-11-01 20:57:07 -0400 |
---|---|---|
committer | jgmdev <jgmdev@gmail.com> | 2022-11-01 20:57:07 -0400 |
commit | 49139e03398c9d0ecb347267a4882a4eb3f7ee23 (patch) | |
tree | 6ac890a209172a29e09ad47ccd0156e7f7114382 /plugins | |
parent | 380f6ef5fe9f8af19cd1f6b4c043eede51cbfcae (diff) | |
parent | 0971a7a686a4e18ee31b576c460966a5ec20ff01 (diff) | |
download | lite-xl-plugins-49139e03398c9d0ecb347267a4882a4eb3f7ee23.tar.gz lite-xl-plugins-49139e03398c9d0ecb347267a4882a4eb3f7ee23.zip |
Merge branch '2.1'
Diffstat (limited to 'plugins')
127 files changed, 6600 insertions, 1878 deletions
diff --git a/plugins/align_carets.lua b/plugins/align_carets.lua index 95d64d5..2a1db2a 100644 --- a/plugins/align_carets.lua +++ b/plugins/align_carets.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local DocView = require "core.docview" diff --git a/plugins/autoinsert.lua b/plugins/autoinsert.lua index 94bcc74..c49887a 100644 --- a/plugins/autoinsert.lua +++ b/plugins/autoinsert.lua @@ -1,20 +1,21 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local translate = require "core.doc.translate" local config = require "core.config" +local common = require "core.common" local DocView = require "core.docview" local command = require "core.command" local keymap = require "core.keymap" -config.plugins.autoinsert = { map = { +config.plugins.autoinsert = common.merge({ map = { ["["] = "]", ["{"] = "}", ["("] = ")", ['"'] = '"', ["'"] = "'", ["`"] = "`", -} } +} }, config.plugins.autoinsert) local function is_closer(chr) diff --git a/plugins/autosave.lua b/plugins/autosave.lua index 9518adf..b63a6f6 100644 --- a/plugins/autosave.lua +++ b/plugins/autosave.lua @@ -1,15 +1,40 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local config = require "core.config" local Doc = require "core.doc" local command = require "core.command" +local common = require "core.common" -- this is used to detect the wait time local last_keypress = os.time() -- this exists so that we don't end up with multiple copies of the loop running at once local looping = false local on_text_change = Doc.on_text_change --- the approximate amount of time, in seconds, that it takes to trigger an autosave -config.plugins.autosave = { timeout = 1 } + +config.plugins.autosave = common.merge({ + enabled = true, + -- the approximate amount of time, in seconds, that it takes to trigger an autosave + timeout = 1, + -- The config specification used by the settings gui + config_spec = { + name = "Auto Save", + { + label = "Enable", + description = "Enable or disable the auto save feature.", + path = "enabled", + type = "toggle", + default = true + }, + { + label = "Timeout", + description = "Approximate amount of time in seconds it takes to trigger an autosave.", + path = "timeout", + type = "number", + default = 1, + min = 1, + max = 30 + } + } +}, config.plugins.autosave) local function loop_for_save() @@ -38,7 +63,7 @@ end function Doc:on_text_change(type) -- check if file is saved - if self.filename then + if config.plugins.autosave.enabled and self.filename then updatepress() end return on_text_change(self, type) diff --git a/plugins/autosaveonfocuslost.lua b/plugins/autosaveonfocuslost.lua index dea1e7c..1a9bd0c 100644 --- a/plugins/autosaveonfocuslost.lua +++ b/plugins/autosaveonfocuslost.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local CommandView = require "core.commandview" local DocView = require "core.docview" diff --git a/plugins/autowrap.lua b/plugins/autowrap.lua index c9dde6d..a7a9765 100644 --- a/plugins/autowrap.lua +++ b/plugins/autowrap.lua @@ -1,10 +1,32 @@ --- mod-version:2 -- lite-xl 2.0 -require "plugins.reflow" +-- mod-version:3 +local core = require "core" local config = require "core.config" local command = require "core.command" +local common = require "core.common" local DocView = require "core.docview" -config.plugins.autowrap = { files = { "%.md$", "%.txt$" } } +config.plugins.autowrap = common.merge({ + enabled = false, + files = { "%.md$", "%.txt$" }, + -- The config specification used by the settings gui + config_spec = { + name = "Auto Wrap", + { + label = "Enable", + description = "Activates text auto wrapping by default.", + path = "enabled", + type = "toggle", + default = false + }, + { + label = "Files", + description = "List of Lua patterns matching files to auto wrap.", + path = "files", + type = "list_strings", + default = { "%.md$", "%.txt$" }, + } + } +}, config.plugins.autowrap) local on_text_input = DocView.on_text_input @@ -12,6 +34,8 @@ local on_text_input = DocView.on_text_input DocView.on_text_input = function(self, ...) on_text_input(self, ...) + if not config.plugins.autowrap.enabled then return end + -- early-exit if the filename does not match a file type pattern local filename = self.doc.filename or "" local matched = false @@ -31,7 +55,17 @@ DocView.on_text_input = function(self, ...) command.perform("doc:select-lines") command.perform("reflow:reflow") command.perform("doc:move-to-next-char") - command.perform("doc:move-to-previous-char") command.perform("doc:move-to-end-of-line") end end + +command.add(nil, { + ["auto-wrap:toggle"] = function() + config.plugins.autowrap.enabled = not config.plugins.autowrap.enabled + if config.plugins.autowrap.enabled then + core.log("Auto wrap: on") + else + core.log("Auto wrap: off") + end + end +}) diff --git a/plugins/bigclock.lua b/plugins/bigclock.lua index c246df5..f3554ac 100644 --- a/plugins/bigclock.lua +++ b/plugins/bigclock.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local style = require "core.style" local command = require "core.command" @@ -7,11 +7,39 @@ local config = require "core.config" local View = require "core.view" -config.plugins.bigclock = { +config.plugins.bigclock = common.merge({ time_format = "%H:%M:%S", date_format = "%A, %d %B %Y", - scale = 1 -} + scale = 1, + -- The config specification used by the settings gui + config_spec = { + name = "Big Clock", + { + label = "Time Format", + description = "Time specification defined with Lua date/time place holders.", + path = "time_format", + type = "string", + default = "%H:%M:%S" + }, + { + label = "Date Format", + description = "Date specification defined with Lua date/time place holders.", + path = "date_format", + type = "string", + default = "%A, %d %B %Y", + }, + { + label = "Scale", + description = "Size of the clock relative to screen.", + path = "scale", + type = "number", + default = 1, + min = 0.5, + max = 3.0, + step = 0.1 + } + } +}, config.plugins.bigclock) local ClockView = View:extend() @@ -21,6 +49,7 @@ function ClockView:new() ClockView.super.new(self) self.time_text = "" self.date_text = "" + self.last_scale = 0 end @@ -30,14 +59,18 @@ end function ClockView:update_fonts() + if self.last_scale ~= config.plugins.bigclock.scale then + self.last_scale = config.plugins.bigclock.scale + else + return + end local size = math.floor(self.size.x * 0.15 / 15) * 15 * config.plugins.bigclock.scale if self.font_size ~= size then - self.time_font = renderer.font.load(DATADIR .. "/fonts/font.ttf", size) - self.date_font = renderer.font.load(DATADIR .. "/fonts/font.ttf", size * 0.3) + self.time_font = renderer.font.copy(style["font"], size) + self.date_font = renderer.font.copy(style["font"], size * 0.3) self.font_size = size collectgarbage() end - return self.font end diff --git a/plugins/bracketmatch.lua b/plugins/bracketmatch.lua index cd9fc2f..6119330 100644 --- a/plugins/bracketmatch.lua +++ b/plugins/bracketmatch.lua @@ -1,10 +1,11 @@ --- mod-version:2 -- lite-xl 2.0 +--- mod-version:3 local core = require "core" local style = require "core.style" local command = require "core.command" local keymap = require "core.keymap" local DocView = require "core.docview" local config = require "core.config" +local common = require "core.common" -- Colors can be configured as follows: -- underline color = `style.bracketmatch_color` @@ -12,12 +13,62 @@ local config = require "core.config" -- background color = `style.bracketmatch_block_color` -- frame color = `style.bracketmatch_frame_color` -config.plugins.bracketmatch = { - highligh_both = true, -- highlight the current bracket too - style = "underline", -- can be "underline", "block", "frame", "none" - color_char = false, -- color the bracket - line_size = math.ceil(1 * SCALE), -- the size of the lines used in "underline" and "frame" -} +config.plugins.bracketmatch = common.merge({ + -- highlight the current bracket too + highlight_both = true, + -- can be "underline", "block", "frame", "none" + style = "underline", + -- color the bracket + color_char = false, + -- the size of the lines used in "underline" and "frame" + line_size = math.ceil(1 * SCALE), + -- The config specification used by the settings gui + config_spec = { + name = "Bracket Match", + { + label = "Highlight Both", + description = "Highlight the current bracket too.", + path = "highlight_both", + type = "toggle", + default = true + }, + { + label = "Style", + description = "The visual indicator for pair brackets.", + path = "style", + type = "selection", + default = "underline", + values = { + {"Underline", "underline"}, + {"Block", "block"}, + {"Frame", "frame"}, + {"None", "none"} + } + }, + { + label = "Colorize Bracket", + description = "Change the color of the matching brackets.", + path = "color_char", + type = "toggle", + default = false + }, + { + label = "Line Size", + description = "Height of the underline on matching brackets.", + path = "line_size", + type = "number", + default = 1, + min = 1, + step = 1, + get_value = function(value) + return math.floor(value / SCALE) + end, + set_value = function(value) + return math.ceil(value * SCALE) + end + } + } +}, config.plugins.bracketmatch) local bracket_maps = { @@ -179,16 +230,17 @@ end local draw_line_text = DocView.draw_line_text -function DocView:draw_line_text(idx, x, y) - draw_line_text(self, idx, x, y) +function DocView:draw_line_text(line, x, y) + local lh = draw_line_text(self, line, x, y) if self.doc == state.doc and state.line2 then - if idx == state.line2 then - draw_decoration(self, x, y, idx, state.col2) + if line == state.line2 then + draw_decoration(self, x, y, line, state.col2) end - if idx == state.line and config.plugins.bracketmatch.highligh_both then - draw_decoration(self, x, y, idx, state.col + select_adj - 1) + if line == state.line and config.plugins.bracketmatch.highlight_both then + draw_decoration(self, x, y, line, state.col + select_adj - 1) end end + return lh end diff --git a/plugins/centerdoc.lua b/plugins/centerdoc.lua index 8e4f8a4..980cbed 100644 --- a/plugins/centerdoc.lua +++ b/plugins/centerdoc.lua @@ -1,21 +1,108 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 +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 treeview = require "plugins.treeview" local DocView = require "core.docview" +config.plugins.centerdoc = common.merge({ + enabled = true, + zen_mode = false +}, config.plugins.centerdoc) local draw_line_gutter = DocView.draw_line_gutter local get_gutter_width = DocView.get_gutter_width -function DocView:draw_line_gutter(idx, x, y, width) - local real_gutter_width = get_gutter_width(self) - local offset = self:get_gutter_width() - real_gutter_width * 2 - draw_line_gutter(self, idx, x + offset, y, real_gutter_width) +function DocView:draw_line_gutter(line, x, y, width) + local lh + if not config.plugins.centerdoc.enabled then + lh = draw_line_gutter(self, line, x, y, width) + else + local real_gutter_width = self:get_font():get_width(#self.doc.lines) + local offset = self:get_gutter_width() - real_gutter_width * 2 + lh = draw_line_gutter(self, line, x + offset, y, real_gutter_width) + end + return lh end function DocView:get_gutter_width() - local real_gutter_width = get_gutter_width(self) - local width = real_gutter_width + self:get_font():get_width("n") * config.line_limit - return math.max((self.size.x - width) / 2, real_gutter_width) + if not config.plugins.centerdoc.enabled then + return get_gutter_width(self) + else + local real_gutter_width, gutter_padding = get_gutter_width(self) + local width = real_gutter_width + self:get_font():get_width("n") * config.line_limit + return math.max((self.size.x - width) / 2, real_gutter_width), gutter_padding + end end + + +local previous_win_status = system.get_window_mode() +local previous_treeview_status = treeview.visible +local previous_statusbar_status = core.status_view.visible + +local function toggle_zen_mode(enabled) + config.plugins.centerdoc.zen_mode = enabled + + if config.plugins.centerdoc.zen_mode then + previous_win_status = system.get_window_mode() + previous_treeview_status = treeview.visible + previous_statusbar_status = core.status_view.visible + + config.plugins.centerdoc.enabled = true + system.set_window_mode("fullscreen") + treeview.visible = false + command.perform "status-bar:hide" + else + config.plugins.centerdoc.enabled = false + system.set_window_mode(previous_win_status) + treeview.visible = previous_treeview_status + core.status_view.visible = previous_statusbar_status + end +end + +local on_startup = true + +-- The config specification used by the settings gui +config.plugins.centerdoc.config_spec = { + name = "Center Document", + { + label = "Enable", + description = "Activates document centering by default.", + path = "enabled", + type = "toggle", + default = true + }, + { + label = "Zen Mode", + description = "Activates zen mode by default.", + path = "zen_mode", + type = "toggle", + default = false, + on_apply = function(enabled) + if on_startup then + core.add_thread(function() + toggle_zen_mode(enabled) + end) + on_startup = false + else + toggle_zen_mode(enabled) + end + end + } +} + + +command.add(nil, { + ["center-doc:toggle"] = function() + config.plugins.centerdoc.enabled = not config.plugins.centerdoc.enabled + end, + ["center-doc:zen-mode-toggle"] = function() + toggle_zen_mode(not config.plugins.centerdoc.zen_mode) + end, +}) + +keymap.add { ["ctrl+alt+z"] = "center-doc:zen-mode-toggle" } diff --git a/plugins/colorpreview.lua b/plugins/colorpreview.lua index c552f07..0aa7663 100644 --- a/plugins/colorpreview.lua +++ b/plugins/colorpreview.lua @@ -1,15 +1,31 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 +local config = require "core.config" local common = require "core.common" local DocView = require "core.docview" +config.plugins.colorpreview = common.merge({ + enabled = true, + -- The config specification used by the settings gui + config_spec = { + name = "Color Preview", + { + label = "Enable", + description = "Enable or disable the color preview feature.", + path = "enabled", + type = "toggle", + default = true + } + } +}, config.plugins.colorpreview) + local white = { common.color "#ffffff" } local black = { common.color "#000000" } local tmp = {} -local function draw_color_previews(self, idx, x, y, ptn, base, nibbles) - local text = self.doc.lines[idx] +local function draw_color_previews(self, line, x, y, ptn, base, nibbles) + local text = self.doc.lines[line] local s, e = 0, 0 while true do @@ -35,8 +51,8 @@ local function draw_color_previews(self, idx, x, y, ptn, base, nibbles) b = b * 16 end - local x1 = x + self:get_col_x_offset(idx, s) - local x2 = x + self:get_col_x_offset(idx, e + 1) + local x1 = x + self:get_col_x_offset(line, s) + local x2 = x + self:get_col_x_offset(line, e + 1) local oy = self:get_line_text_y_offset() local text_color = math.max(r, g, b) < 128 and white or black @@ -44,7 +60,7 @@ local function draw_color_previews(self, idx, x, y, ptn, base, nibbles) local l1, _, l2, _ = self.doc:get_selection(true) - if not (self.doc:has_selection() and idx >= l1 and idx <= l2) then + if not (self.doc:has_selection() and line >= l1 and line <= l2) then renderer.draw_rect(x1, y, x2 - x1, self:get_line_height(), tmp) renderer.draw_text(self:get_font(), str, x1, y + oy, text_color) end @@ -54,9 +70,19 @@ end local draw_line_text = DocView.draw_line_text -function DocView:draw_line_text(idx, x, y) - draw_line_text(self, idx, x, y) - draw_color_previews(self, idx, x, y, "#(%x%x)(%x%x)(%x%x)(%x?%x?)%f[%W]", 16) - draw_color_previews(self, idx, x, y, "#(%x)(%x)(%x)%f[%W]", 16, true) -- support #fff css format - draw_color_previews(self, idx, x, y, "rgba?%((%d+)%D+(%d+)%D+(%d+)[%s,]-([%.%d]-)%s-%)", nil) +function DocView:draw_line_text(line, x, y) + local lh = draw_line_text(self, line, x, y) + if config.plugins.colorpreview.enabled then + draw_color_previews(self, line, x, y, + "#(%x%x)(%x%x)(%x%x)(%x?%x?)%f[%W]", + 16 + ) + -- support #fff css format + draw_color_previews(self, line, x, y, "#(%x)(%x)(%x)%f[%W]", 16, true) + draw_color_previews(self, line, x, y, + "rgba?%((%d+)%D+(%d+)%D+(%d+)[%s,]-([%.%d]-)%s-%)", + nil + ) + end + return lh end diff --git a/plugins/copyfilelocation.lua b/plugins/copyfilelocation.lua index dedc188..eb7b1a9 100644 --- a/plugins/copyfilelocation.lua +++ b/plugins/copyfilelocation.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" diff --git a/plugins/datetimestamps.lua b/plugins/datetimestamps.lua index 51d698e..f16af83 100644 --- a/plugins/datetimestamps.lua +++ b/plugins/datetimestamps.lua @@ -1,7 +1,8 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local config = require "core.config" local command = require "core.command" +local common = require "core.common" --[[ Date and time format placeholders @@ -25,11 +26,36 @@ from https://www.lua.org/pil/22.1.html %y two-digit year (98) [00-99] %% the character `%´ --]] -config.plugins.datetimestamps = { - format_datestamp = "%Y%m%d" - format_datetimestamp = "%Y%m%d_%H%M%S" - format_timestamp = "%H%M%S" -} +config.plugins.datetimestamps = common.merge({ + format_datestamp = "%Y%m%d", + format_datetimestamp = "%Y%m%d_%H%M%S", + format_timestamp = "%H%M%S", + -- The config specification used by the settings gui + config_spec = { + name = "Date and Time Stamps", + { + label = "Date", + description = "Date specification defined with Lua date/time place holders.", + path = "format_datestamp", + type = "string", + default = "%Y%m%d" + }, + { + label = "Time", + description = "Time specification defined with Lua date/time place holders.", + path = "format_timestamp", + type = "string", + default = "%H%M%S" + }, + { + label = "Date and Time", + description = "Date and time specification defined with Lua date/time place holders.", + path = "format_datetimestamp", + type = "string", + default = "%Y%m%d_%H%M%S" + } + } +}, config.plugins.datetimestamps) local function datestamp() local sOut = os.date(config.plugins.datetimestamps.format_datestamp) @@ -49,6 +75,13 @@ end command.add("core.docview", { ["datetimestamps:insert-datestamp"] = datestamp, ["datetimestamps:insert-timestamp"] = timestamp, - ["datetimestamps:insert-datetimestamp"] = datetimestamp + ["datetimestamps:insert-datetimestamp"] = datetimestamp, + ["datetimestamps:insert-custom"] = function() + core.command_view:enter("Date format eg: %H:%M:%S", { + submit = function(cmd) + core.active_view.doc:text_input(os.date(cmd) or "") + end + }) + end, }) diff --git a/plugins/dragdropselected.lua b/plugins/dragdropselected.lua index 6bdb101..3c6583b 100644 --- a/plugins/dragdropselected.lua +++ b/plugins/dragdropselected.lua @@ -1,13 +1,13 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 --[[ - dragdropselected.lua - provides basic drag and drop of selected text (in same document) - version: 20200627_133351 - originally by SwissalpS - - TODO: use OS drag and drop events - TODO: change mouse cursor when duplicating - TODO: add dragging image + dragdropselected.lua + provides basic drag and drop of selected text (in same document) + version: 20200627_133351 + originally by SwissalpS + + TODO: use OS drag and drop events + TODO: change mouse cursor when duplicating + TODO: add dragging image --]] local DocView = require "core.docview" local core = require "core" @@ -22,16 +22,16 @@ local style = require "core.style" -- iSelLine2 is line number where selection ends -- iSelCol2 is column where selection ends local function isInSelection(iLine, iCol, iSelLine1, iSelCol1, iSelLine2, iSelCol2) - if iLine < iSelLine1 then return false end - if iLine > iSelLine2 then return false end - if (iLine == iSelLine1) and (iCol < iSelCol1) then return false end - if (iLine == iSelLine2) and (iCol > iSelCol2) then return false end - return true + if iLine < iSelLine1 then return false end + if iLine > iSelLine2 then return false end + if (iLine == iSelLine1) and (iCol < iSelCol1) then return false end + if (iLine == iSelLine2) and (iCol > iSelCol2) then return false end + return true end -- isInSelection -- distance between two points local function distance(x1, y1, x2, y2) - return math.sqrt(math.pow(x2-x1, 2)+math.pow(y2-y1, 2)) + return math.sqrt(math.pow(x2-x1, 2)+math.pow(y2-y1, 2)) end local min_drag = style.code_font:get_width(" ") @@ -40,126 +40,138 @@ local min_drag = style.code_font:get_width(" ") local on_mouse_moved = DocView.on_mouse_moved function DocView:on_mouse_moved(x, y, ...) - local sCursor = nil - - -- make sure we only act if previously on_mouse_pressed was in selection - if self.bClickedIntoSelection and - ( -- we are already dragging or we moved enough to start dragging - not self.drag_start_loc or - distance(self.drag_start_loc[1],self.drag_start_loc[2], x, y) > min_drag - ) then - self.drag_start_loc = nil - - -- show that we are dragging something - sCursor = 'hand' - - -- calculate line and column for current mouse position - local iLine, iCol = self:resolve_screen_position(x, y) - local iSelLine1 = self.dragged_selection[1] - local iSelCol1 = self.dragged_selection[2] - local iSelLine2 = self.dragged_selection[3] - local iSelCol2 = self.dragged_selection[4] - self.doc:set_selection(iSelLine1, iSelCol1, iSelLine2, iSelCol2) - if not isInSelection(iLine, iCol, iSelLine1, iSelCol1, iSelLine2, iSelCol2) then - -- show cursor only if outside selection - self.doc:add_selection(iLine, iCol) - end - -- update scroll position - self:scroll_to_line(iLine, true) - end -- if previously clicked into selection + local sCursor = nil + + -- make sure we only act if previously on_mouse_pressed was in selection + if + self.bClickedIntoSelection + and + ( -- we are already dragging or we moved enough to start dragging + not self.drag_start_loc or + distance(self.drag_start_loc[1],self.drag_start_loc[2], x, y) > min_drag + ) + then + self.drag_start_loc = nil - -- hand off to 'old' on_mouse_moved() - on_mouse_moved(self, x, y, ...) - -- override cursor as needed - if sCursor then self.cursor = sCursor end + -- show that we are dragging something + sCursor = 'hand' + + -- calculate line and column for current mouse position + local iLine, iCol = self:resolve_screen_position(x, y) + local iSelLine1 = self.dragged_selection[1] + local iSelCol1 = self.dragged_selection[2] + local iSelLine2 = self.dragged_selection[3] + local iSelCol2 = self.dragged_selection[4] + self.doc:set_selection(iSelLine1, iSelCol1, iSelLine2, iSelCol2) + if not isInSelection(iLine, iCol, iSelLine1, iSelCol1, iSelLine2, iSelCol2) then + -- show cursor only if outside selection + self.doc:add_selection(iLine, iCol) + end + -- update scroll position + self:scroll_to_line(iLine, true) + end -- if previously clicked into selection + + -- hand off to 'old' on_mouse_moved() + on_mouse_moved(self, x, y, ...) + -- override cursor as needed + if sCursor then self.cursor = sCursor end end -- DocView:on_mouse_moved -- override DocView:on_mouse_pressed local on_mouse_pressed = DocView.on_mouse_pressed function DocView:on_mouse_pressed(button, x, y, clicks) - local caught = DocView.super.on_mouse_pressed(self, button, x, y, clicks) - if caught then - return caught - end - -- no need to proceed if not left button or has no selection - if ('left' ~= button) - or (not self.doc:has_selection()) - or (1 < clicks) then - return on_mouse_pressed(self, button, x, y, clicks) - end - -- convert pixel coordinates to line and column coordinates - local iLine, iCol = self:resolve_screen_position(x, y) - -- get selection coordinates - local iSelLine1, iSelCol1, iSelLine2, iSelCol2 = self.doc:get_selection(true) - -- set flag for on_mouse_released and on_mouse_moved() methods to detect dragging - self.bClickedIntoSelection = isInSelection(iLine, iCol, iSelLine1, iSelCol1, - iSelLine2, iSelCol2) - if self.bClickedIntoSelection then - self.drag_start_loc = { x, y } - -- stash selection for inserting later - self.sDraggedText = self.doc:get_text(self.doc:get_selection()) - self.dragged_selection = { iSelLine1, iSelCol1, iSelLine2, iSelCol2 } - else - self.bClickedIntoSelection = nil - self.dragged_selection = nil - -- let 'old' on_mouse_pressed() do whatever it needs to do - on_mouse_pressed(self, button, x, y, clicks) - end + local caught = DocView.super.on_mouse_pressed(self, button, x, y, clicks) + if caught then + return caught + end + -- no need to proceed if not left button or has no selection + if + ('left' ~= button) + or (not self.doc:has_selection()) + or (1 < clicks) + then + return on_mouse_pressed(self, button, x, y, clicks) + end + -- convert pixel coordinates to line and column coordinates + local iLine, iCol = self:resolve_screen_position(x, y) + -- get selection coordinates + local iSelLine1, iSelCol1, iSelLine2, iSelCol2 = self.doc:get_selection(true) + -- set flag for on_mouse_released and on_mouse_moved() methods to detect dragging + self.bClickedIntoSelection = isInSelection(iLine, iCol, iSelLine1, iSelCol1, + iSelLine2, iSelCol2) + if self.bClickedIntoSelection then + self.drag_start_loc = { x, y } + -- stash selection for inserting later + self.sDraggedText = self.doc:get_text(self.doc:get_selection()) + self.dragged_selection = { iSelLine1, iSelCol1, iSelLine2, iSelCol2 } + else + self.bClickedIntoSelection = nil + self.dragged_selection = nil + -- let 'old' on_mouse_pressed() do whatever it needs to do + on_mouse_pressed(self, button, x, y, clicks) + end end -- DocView:on_mouse_pressed -- override DocView:on_mouse_released() local on_mouse_released = DocView.on_mouse_released function DocView:on_mouse_released(button, x, y) - local iLine, iCol = self:resolve_screen_position(x, y) - if self.bClickedIntoSelection then - local iSelLine1, iSelCol1, iSelLine2, iSelCol2 = table.unpack(self.dragged_selection) - if not self.drag_start_loc - and not isInSelection(iLine, iCol, iSelLine1, iSelCol1, iSelLine2, iSelCol2) then - -- insert stashed selected text at current position - if iLine < iSelLine1 or (iLine == iSelLine1 and iCol < iSelCol1) then - -- delete first - self.doc:set_selection(iSelLine1, iSelCol1, iSelLine2, iSelCol2) - if not keymap.modkeys['ctrl'] then - self.doc:delete_to(0) - end - self.doc:set_selection(iLine, iCol) - self.doc:text_input(self.sDraggedText) - else - -- insert first - self.doc:set_selection(iLine, iCol) - self.doc:text_input(self.sDraggedText) - self.doc:set_selection(iSelLine1, iSelCol1, iSelLine2, iSelCol2) - if not keymap.modkeys['ctrl'] then - self.doc:delete_to(0) - end - self.doc:set_selection(iLine, iCol) - end - elseif self.drag_start_loc then - -- deselect only if the drag never happened - self.doc:set_selection(iLine, iCol) + local iLine, iCol = self:resolve_screen_position(x, y) + if self.bClickedIntoSelection then + local iSelLine1, iSelCol1, iSelLine2, iSelCol2 = table.unpack(self.dragged_selection) + if + not self.drag_start_loc + and + not isInSelection(iLine, iCol, iSelLine1, iSelCol1, iSelLine2, iSelCol2) + then + -- insert stashed selected text at current position + if iLine < iSelLine1 or (iLine == iSelLine1 and iCol < iSelCol1) then + -- delete first + self.doc:set_selection(iSelLine1, iSelCol1, iSelLine2, iSelCol2) + if not keymap.modkeys['ctrl'] then + self.doc:delete_to(0) end - -- unset stash and flag(s) TODO: - self.sDraggedText = '' - self.bClickedIntoSelection = nil + self.doc:set_selection(iLine, iCol) + self.doc:text_input(self.sDraggedText) + else + -- insert first + self.doc:set_selection(iLine, iCol) + self.doc:text_input(self.sDraggedText) + self.doc:set_selection(iSelLine1, iSelCol1, iSelLine2, iSelCol2) + if not keymap.modkeys['ctrl'] then + self.doc:delete_to(0) + end + self.doc:set_selection(iLine, iCol) + end + elseif self.drag_start_loc then + -- deselect only if the drag never happened + self.doc:set_selection(iLine, iCol) end + -- unset stash and flag(s) TODO: + self.sDraggedText = '' + self.bClickedIntoSelection = nil + end - -- hand over to old handler - on_mouse_released(self, button, x, y) + -- hand over to old handler + on_mouse_released(self, button, x, y) end -- DocView:on_mouse_released -- override DocView:draw_caret() local draw_caret = DocView.draw_caret function DocView:draw_caret(x, y) - if self.bClickedIntoSelection then - local iLine, iCol = self:resolve_screen_position(x, y) - -- don't show carets inside selections - if isInSelection(iLine, iCol, - self.dragged_selection[1], self.dragged_selection[2], - self.dragged_selection[3], self.dragged_selection[4]) then - return - end + if self.bClickedIntoSelection then + local iLine, iCol = self:resolve_screen_position(x, y) + -- don't show carets inside selections + if + isInSelection( + iLine, iCol, + self.dragged_selection[1], self.dragged_selection[2], + self.dragged_selection[3], self.dragged_selection[4] + ) + then + return end - draw_caret(self, x, y) + end + draw_caret(self, x, y) end -- DocView:draw_caret() diff --git a/plugins/ephemeral_tabs.lua b/plugins/ephemeral_tabs.lua index dea4261..f61c493 100644 --- a/plugins/ephemeral_tabs.lua +++ b/plugins/ephemeral_tabs.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local RootView = require "core.rootview" diff --git a/plugins/eval.lua b/plugins/eval.lua index bd1ff56..c2eb19e 100644 --- a/plugins/eval.lua +++ b/plugins/eval.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" @@ -13,12 +13,20 @@ end command.add("core.docview", { ["eval:insert"] = function() - core.command_view:enter("Evaluate And Insert Result", function(cmd) - core.active_view.doc:text_input(eval(cmd)) - end) + core.command_view:enter("Evaluate And Insert Result", { + submit = function(cmd) + core.active_view.doc:text_input(eval(cmd)) + end + }) end, ["eval:replace"] = function() - core.active_view.doc:replace(eval) + core.command_view:enter("Evaluate And Replace With Result", { + submit = function(cmd) + core.active_view.doc:replace(function(str) + return eval(cmd) + end) + end + }) end, }) diff --git a/plugins/exec.lua b/plugins/exec.lua index cf2d8f2..b8f61c5 100644 --- a/plugins/exec.lua +++ b/plugins/exec.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" @@ -28,19 +28,23 @@ end command.add("core.docview", { ["exec:insert"] = function() - core.command_view:enter("Insert Result Of Command", function(cmd) - core.active_view.doc:text_input(exec(cmd)) - end) + core.command_view:enter("Insert Result Of Command", { + submit = function(cmd) + core.active_view.doc:text_input(exec(cmd)) + end + }) end, ["exec:replace"] = function() - core.command_view:enter("Replace With Result Of Command", function(cmd) - core.active_view.doc:replace(function(str) - return exec( - "printf %b " .. printfb_quote(str:gsub("%\n$", "") .. "\n") .. " | eval '' " .. shell_quote(cmd), - str:find("%\n$") - ) - end) - end) + core.command_view:enter("Replace With Result Of Command", { + submit = function(cmd) + core.active_view.doc:replace(function(str) + return exec( + "printf %b " .. printfb_quote(str:gsub("%\n$", "") .. "\n") .. " | eval '' " .. shell_quote(cmd), + str:find("%\n$") + ) + end) + end + }) end, }) diff --git a/plugins/extend_selection_line.lua b/plugins/extend_selection_line.lua index e986597..e002674 100644 --- a/plugins/extend_selection_line.lua +++ b/plugins/extend_selection_line.lua @@ -1,19 +1,20 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local DocView = require "core.docview" local style = require "core.style" local draw_line_body = DocView.draw_line_body -function DocView:draw_line_body(idx, x, y, ...) - draw_line_body(self, idx, x, y, ...) +function DocView:draw_line_body(line, x, y) + local line_height = draw_line_body(self, line, x, y) local lh = self:get_line_height() for _, line1, _, line2, _ in self.doc:get_selections(true) do - if idx >= line1 and idx < line2 and line1 ~= line2 then + if line >= line1 and line < line2 and line1 ~= line2 then -- draw selection from the end of the line to the end of the available space - local x1 = x + self:get_col_x_offset(idx, #self.doc.lines[idx]) + local x1 = x + self:get_col_x_offset(line, #self.doc.lines[line]) local x2 = x + self.scroll.x + self.size.x if x2 > x1 then renderer.draw_rect(x1, y, x2 - x1, lh, style.selection) end end end + return line_height end diff --git a/plugins/fontconfig.lua b/plugins/fontconfig.lua index 657e364..8b713b5 100644 --- a/plugins/fontconfig.lua +++ b/plugins/fontconfig.lua @@ -1,11 +1,12 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local subprocess = require "process" local core = require "core" local style = require "core.style" local config = require "core.config" +local common = require "core.common" -config.plugins.fontconfig = { prefix = "" } +config.plugins.fontconfig = common.merge({ prefix = "" }, config.plugins.fontconfig) --[[ Example config (put it in user module): diff --git a/plugins/force_syntax.lua b/plugins/force_syntax.lua index dce4abc..ae5a138 100644 --- a/plugins/force_syntax.lua +++ b/plugins/force_syntax.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local Doc = require "core.doc" local syntax = require "core.syntax" @@ -39,30 +39,24 @@ local function get_syntax_name(s) return name or "Undefined" end -local statusview_get_items = StatusView.get_items -function StatusView:get_items() - local left, right = statusview_get_items(self) - - local is_dv = core.active_view and getmetatable(core.active_view) == DocView - if not is_dv then return left, right end - - local syntax_name = get_syntax_name(doc().syntax) - - local ins = { - style.dim, - self.separator2, - style.text, - syntax_name - } - - if syntax_name then - for _,item in pairs(ins) do - table.insert(right, item) - end - end - - return left, right -end +core.status_view:add_item({ + predicate = function() + return core.active_view and getmetatable(core.active_view) == DocView + end, + name = "doc:syntax", + alignment = StatusView.Item.RIGHT, + get_item = function() + local syntax_name = get_syntax_name(doc().syntax) + return { + style.text, + syntax_name + } + end, + command = "force-syntax:select-file-syntax", + position = -1, + tooltip = "file syntax", + separator = core.status_view.separator2 +}) local function get_syntax_list() local pt_name = plain_text_syntax.name @@ -110,23 +104,20 @@ end command.add("core.docview", { ["force-syntax:select-file-syntax"] = function() - core.command_view:enter( - "Set syntax for this file", - function(text, item) -- submit + core.command_view:enter("Set syntax for this file", { + submit = function(text, item) local list, _ = get_syntax_list() doc().force_syntax = list[item.text] doc():reset_syntax() end, - function(text) -- suggest + suggest = function(text) local _, keylist = get_syntax_list() local res = common.fuzzy_match(keylist, text) -- Force Current and Auto detect syntax to the bottom -- if the text is empty table.sort(res, #text == 0 and bias_sorter or sorter) return res - end, - nil, -- cancel - nil -- validate - ) + end + }) end }) diff --git a/plugins/ghmarkdown.lua b/plugins/ghmarkdown.lua index 532be57..244b144 100644 --- a/plugins/ghmarkdown.lua +++ b/plugins/ghmarkdown.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" diff --git a/plugins/gitstatus.lua b/plugins/gitstatus.lua index de5c74b..9a36142 100644 --- a/plugins/gitstatus.lua +++ b/plugins/gitstatus.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local common = require "core.common" local config = require "core.config" @@ -6,15 +6,37 @@ local style = require "core.style" local StatusView = require "core.statusview" local TreeView = require "plugins.treeview" +config.plugins.gitstatus = common.merge({ + recurse_submodules = true, + -- The config specification used by the settings gui + config_spec = { + name = "Git Status", + { + label = "Recurse Submodules", + description = "Also retrieve git stats from submodules.", + path = "recurse_submodules", + type = "toggle", + default = true + } + } +}, config.plugins.gitstatus) + +style.gitstatus_addition = {common.color "#587c0c"} +style.gitstatus_modification = {common.color "#0c7d9d"} +style.gitstatus_deletion = {common.color "#94151b"} + local scan_rate = config.project_scan_rate or 5 local cached_color_for_item = {} --- Override TreeView's color_for_item, but first --- stash the old one (using [] in case it is not there at all) -local old_color_for_item = TreeView["color_for_item"] -function TreeView:color_for_item(abs_path) - return cached_color_for_item[abs_path] or old_color_for_item(abs_path) +-- Override TreeView's get_item_text to add modification color +local treeview_get_item_text = TreeView.get_item_text +function TreeView:get_item_text(item, active, hovered) + local text, font, color = treeview_get_item_text(self, item, active, hovered) + if cached_color_for_item[item.abs_filename] then + color = cached_color_for_item[item.abs_filename] + end + return text, font, color end @@ -24,15 +46,6 @@ local git = { deletes = 0, } - -config.gitstatus = { - recurse_submodules = true -} -style.gitstatus_addition = {common.color "#587c0c"} -style.gitstatus_modification = {common.color "#0c7d9d"} -style.gitstatus_deletion = {common.color "#94151b"} - - local function exec(cmd) local proc = process.start(cmd) -- Don't use proc:wait() here - that will freeze the app. @@ -57,7 +70,11 @@ core.add_thread(function() -- get diff local diff = exec({"git", "diff", "--numstat"}) - if config.gitstatus.recurse_submodules and system.get_file_info(".gitmodules") then + if + config.plugins.gitstatus.recurse_submodules + and + system.get_file_info(".gitmodules") + then local diff2 = exec({"git", "submodule", "foreach", "git diff --numstat"}) diff = diff .. diff2 end @@ -99,27 +116,23 @@ core.add_thread(function() end) -local get_items = StatusView.get_items - -function StatusView:get_items() - if not git.branch then - return get_items(self) - end - local left, right = get_items(self) - - local t = { - style.dim, self.separator, - (git.inserts ~= 0 or git.deletes ~= 0) and style.accent or style.text, - git.branch, - style.dim, " ", - git.inserts ~= 0 and style.accent or style.text, "+", git.inserts, - style.dim, " / ", - git.deletes ~= 0 and style.accent or style.text, "-", git.deletes, - } - for _, item in ipairs(t) do - table.insert(right, item) - end - - return left, right -end - +core.status_view:add_item({ + name = "status:git", + alignment = StatusView.Item.RIGHT, + get_item = function() + if not git.branch then + return {} + end + return { + (git.inserts ~= 0 or git.deletes ~= 0) and style.accent or style.text, + git.branch, + style.dim, " ", + git.inserts ~= 0 and style.accent or style.text, "+", git.inserts, + style.dim, " / ", + git.deletes ~= 0 and style.accent or style.text, "-", git.deletes, + } + end, + position = -1, + tooltip = "branch and changes", + separator = core.status_view.separator2 +}) diff --git a/plugins/gofmt.lua b/plugins/gofmt.lua index 02c817b..fec95e4 100644 --- a/plugins/gofmt.lua +++ b/plugins/gofmt.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" diff --git a/plugins/hidelinenumbers.lua b/plugins/hidelinenumbers.lua deleted file mode 100644 index 4494670..0000000 --- a/plugins/hidelinenumbers.lua +++ /dev/null @@ -1,6 +0,0 @@ --- mod-version:2 -- lite-xl 2.0 -local style = require "core.style" -local DocView = require "core.docview" - -DocView.draw_line_gutter = function() end -DocView.get_gutter_width = function() return style.padding.x end diff --git a/plugins/hidestatus.lua b/plugins/hidestatus.lua deleted file mode 100644 index 6e63a81..0000000 --- a/plugins/hidestatus.lua +++ /dev/null @@ -1,19 +0,0 @@ --- mod-version:2 -- lite-xl 2.0 -local command = require "core.command" -local StatusView = require "core.statusview" - -local visible = false -local funcs = { - [true] = StatusView.update, - [false] = function(self) self.size.y = 0 end, -} - -function StatusView:update(...) - funcs[visible](self, ...) -end - -command.add(nil, { - ["hide-status:toggle"] = function() visible = not visible end, - ["hide-status:hide"] = function() visible = false end, - ["hide-status:show"] = function() visible = true end, -}) diff --git a/plugins/indent_convert.lua b/plugins/indent_convert.lua index c86686d..3a950f5 100644 --- a/plugins/indent_convert.lua +++ b/plugins/indent_convert.lua @@ -1,11 +1,24 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" +local common = require "core.common" local config = require "core.config" local command = require "core.command" -config.plugins.indent_convert = { - update_indent_type = true -- set to false to avoid updating the document indent type -} +config.plugins.indent_convert = common.merge({ + -- set to false to avoid updating the document indent type + update_indent_type = true, + -- The config specification used by the settings gui + config_spec = { + name = "Indent Convert", + { + label = "Update Indent Type", + description = "Disable to avoid updating the document indent type.", + path = "update_indent_type", + type = "toggle", + default = true + } + } +}, config.plugins.indent_convert) local zero_pattern = _VERSION == "Lua 5.1" and "%z" or "\0" diff --git a/plugins/indentguide.lua b/plugins/indentguide.lua index 99b1311..42eb3a6 100644 --- a/plugins/indentguide.lua +++ b/plugins/indentguide.lua @@ -1,8 +1,23 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local style = require "core.style" local config = require "core.config" +local common = require "core.common" local DocView = require "core.docview" +config.plugins.indentguide = common.merge({ + enabled = true, + -- The config specification used by the settings gui + config_spec = { + name = "Indent Guide", + { + label = "Enable", + description = "Toggle the drawing of indentation indicator lines.", + path = "enabled", + type = "toggle", + default = true + } + } +}, config.plugins.indentguide) -- TODO: replace with `doc:get_indent_info()` when 2.1 releases local function get_indent_info(doc) @@ -13,15 +28,15 @@ local function get_indent_info(doc) end -local function get_line_spaces(doc, idx, dir) +local function get_line_spaces(doc, line, dir) local _, indent_size = get_indent_info(doc) - local text = doc.lines[idx] + 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, idx + dir, dir) + return get_line_spaces(doc, line + dir, dir) end local n = 0 for _,b in pairs({text:byte(s, e)}) do @@ -31,25 +46,29 @@ local function get_line_spaces(doc, idx, dir) end -local function get_line_indent_guide_spaces(doc, idx) - if doc.lines[idx]:find("^%s*\n") then +local function get_line_indent_guide_spaces(doc, line) + if doc.lines[line]:find("^%s*\n") then return math.max( - get_line_spaces(doc, idx - 1, -1), - get_line_spaces(doc, idx + 1, 1)) + get_line_spaces(doc, line - 1, -1), + get_line_spaces(doc, line + 1, 1)) end - return get_line_spaces(doc, idx) + return get_line_spaces(doc, line) end local docview_update = DocView.update function DocView:update() docview_update(self) - local function get_indent(idx) - if idx < 1 or idx > #self.doc.lines then return -1 end - if not self.indentguide_indents[idx] then - self.indentguide_indents[idx] = get_line_indent_guide_spaces(self.doc, idx) + if not config.plugins.indentguide.enabled or not self:is(DocView) then + return + end + + 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) end - return self.indentguide_indents[idx] + return self.indentguide_indents[line] end self.indentguide_indents = {} @@ -103,21 +122,23 @@ end local draw_line_text = DocView.draw_line_text -function DocView:draw_line_text(idx, x, y) - local spaces = self.indentguide_indents[idx] or -1 - local _, indent_size = get_indent_info(self.doc) - local w = math.max(1, SCALE) - 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[idx] or -1 - if i < active_lvl and i + indent_size >= active_lvl then - color = style.guide_highlight or style.accent +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 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 + color = style.guide_highlight or style.accent + end + local sw = space_sz * i + renderer.draw_rect(math.ceil(x + sw), y, w, h, color) end - local sw = space_sz * i - renderer.draw_rect(math.ceil(x + sw), y, w, h, color) end - draw_line_text(self, idx, x, y) + return draw_line_text(self, line, x, y) end diff --git a/plugins/ipc.lua b/plugins/ipc.lua new file mode 100644 index 0000000..8a68fc2 --- /dev/null +++ b/plugins/ipc.lua @@ -0,0 +1,1043 @@ +-- mod-version:3 +-- +-- Crossplatform file based IPC system for lite-xl. +-- @copyright Jefferson Gonzalez <jgmdev@gmail.com> +-- @license MIT +-- +local core = require "core" +local config = require "core.config" +local common = require "core.common" +local command = require "core.command" +local Object = require "core.object" +local RootView = require "core.rootview" +local settings_found, settings = pcall(require, "plugins.settings") + +---The maximum amount of seconds a message will be broadcasted. +---@type integer +local MESSAGE_EXPIRATION=3 + +---@class config.plugins.ipc +---@field single_instance boolean +---@field dirs_instance string +config.plugins.ipc = common.merge({ + single_instance = true, + dirs_instance = "new", + -- The config specification used by the settings gui + config_spec = { + name = "Inter-process communication", + { + label = "Single Instance", + description = "Run a single instance of lite-xl.", + path = "single_instance", + type = "toggle", + default = true + }, + { + label = "Directories Instance", + description = "Control how to open directories in single instance mode.", + path = "dirs_instance", + type = "selection", + default = "new", + values = { + {"Create a New Instance", "new"}, + {"Add to Current Instance", "add"}, + {"Change Current Instance Project Directory", "change"} + } + } + } +}, config.plugins.ipc) + +---@alias plugins.ipc.onmessageread fun(message: plugins.ipc.message) | nil +---@alias plugins.ipc.onreplyread fun(reply: plugins.ipc.reply) | nil +---@alias plugins.ipc.onmessage fun(message: plugins.ipc.message, reply: plugins.ipc.reply) | nil +---@alias plugins.ipc.onreply fun(reply: plugins.ipc.reply) | nil +---@alias plugins.ipc.function fun(...) + +---@alias plugins.ipc.messagetype +---| '"message"' +---| '"method"' +---| '"signal"' + +---@class plugins.ipc.message +---@field id string +---@field sender string +---@field name string +---@field type plugins.ipc.messagetype | string +---@field destinations table<integer,string> +---@field data table<string,any> +---@field timestamp number +---@field on_read plugins.ipc.onmessageread +---@field on_reply plugins.ipc.onreply +---@field replies plugins.ipc.reply[] +local IPCMessage = { + ---Id of the message + id = "", + ---The id of process that sent the message + sender = "", + ---Name of the message + name = "", + ---Type of message. + type = "", + ---List with id of the instance that should receive the message. + destinations = {}, + ---A list of named values sent to receivers. + data = {}, + ---Time in seconds when the message was sent for automatic expiration purposes. + timestamp = 0, + ---Optional callback executed by the receiver when the message is read. + on_read = function(message) end, + ---Optional callback executed when a reply to the message is received. + on_reply = function(reply) end, + ---The received replies for the message. + replies = {} +} + +---@class plugins.ipc.reply +---@field id string +---@field sender string +---@field replier string +---@field data table<string,any> +---@field timestamp number +---@field on_read plugins.ipc.onreplyread +local IPCReply = { + ---Id of the message + id = "", + ---The id of process that sent the message + sender = "", + ---The id of the replier + replier = "", + ---A list of named values sent back to sender. + data = {}, + ---Time in seconds when the reply was sent for automatic expiration purposes. + timestamp = 0, + ---Optional callback executed by the sender when the reply is read. + on_read = function(reply) end +} + +---@class plugins.ipc.instance +---@field id string +---@field position integer +---@field last_update integer +---@field messages plugins.ipc.message[] +---@field replies plugins.ipc.reply[] +---@field properties table +local IPCInstance = { + ---Process id of the instance. + id = "", + ---The position in which the instance was launched. + position = 0, + ---Flag that indicates if this instance was the first started. + primary = false, + ---Indicates the last time this instance updated its session file. + last_update = 0, + ---The messages been broadcasted. + messages = {}, + ---The replies been broadcasted. + replies = {}, + ---Table of properties associated with the instance. + properties = {} +} + +---@class core.ipc : core.object +---@field private id string +---@field private user_dir string +---@field private running boolean +---@field private file string +---@field private primary boolean +---@field private position integer +---@field private messages plugins.ipc.message[] +---@field private replies plugins.ipc.reply[] +---@field private listeners table<string,table<integer,plugins.ipc.onmessage>> +---@field private signals table<string,integer> +---@field private methods table<string,integer> +---@field private signal_definitions table<integer,string> +---@field private method_definitions table<integer,string> +local IPC = Object:extend() + +---@class plugins.ipc.threads +---@field cr thread +---@field wake number + +---List of threads belonging to all instantiated IPC objects. +---@type plugins.ipc.threads[] +local threads = {} + +---Register a new thread to be run on the background. +---@param f function +local function add_thread(f) + local key = #threads + 1 + threads[key] = { cr = coroutine.create(f), wake = 0 } + return key +end + +---Updates the session file of an IPC object. +---@param self core.ipc +local function update_file(self) + local file, errmsg = io.open(self.file, "w+") + + if file then + local output = "-- Warning: Generated by IPC system do not edit manually!\n" + .. "return " .. common.serialize( + { + id = self.id, + primary = self.primary, + position = self.position, + last_update = os.time(), + messages = self.messages, + replies = self.replies, + signals = self.signal_definitions, + methods = self.method_definitions + }, + { + pretty = true + } + ) + + output = output:gsub("%s+%[\"on_reply\"%].-\n", "") + + file:write(output) + file:close() + else + core.error("IPC Error: failed updating status (%s)", errmsg) + end +end + +---Constructor +---@param id? string Defaults to current lite-xl process id. +function IPC:new(id) + self.id = id or tostring(system.get_process_id()) + self.user_dir = USERDIR .. "/ipc" + self.file = self.user_dir .. "/" .. self.id .. ".lua" + self.primary = false + self.running = false + self.messages = {} + self.replies = {} + self.listeners = {} + self.signals = {} + self.methods = {} + self.signal_definitions = {} + self.method_definitions = {} + + local ipc_dir_status = system.get_file_info(self.user_dir) + + if not ipc_dir_status then + local created, errmsg = common.mkdirp(self.user_dir) + if not created then + core.error("Error initializing IPC system: %s", errmsg) + return + end + end + + local file, errmsg = io.open(self.file, "w+") + + if not file then + core.error("Error initializing IPC system: %s", errmsg) + return + else + file:close() + os.remove(self.file) + end + + -- Execute to set the instance position and primary attribute if no other running. + local instances = self:get_instances() + self.primary = #instances == 0 and true or false + self.position = #instances + 1 + + self:start() +end + +---Starts and registers the ipc session and monitoring. +function IPC:start() + if not self.running then + self.running = true + + update_file(self) + + local wait_time = 0.3 + + local this = self + self.coroutine_key = add_thread(function() + coroutine.yield(wait_time) + while(true) do + this:read_messages() + this:read_replies() + update_file(this) + coroutine.yield(wait_time) + end + end) + end +end + +---Stop and unregister the ipc session and monitoring. +function IPC:stop() + self.running = false + table.remove(threads, self.coroutine_key) + os.remove(self.file) +end + +---Get a list of running lite-xl instances. +---@return plugins.ipc.instance[] +function IPC:get_instances() + ---@type plugins.ipc.instance[] + local instances = {} + + local files, errmsg = system.list_dir(self.user_dir) + + if files then + for _, file in ipairs(files) do + if string.match(file, "^%d+%.lua$") then + local path = self.user_dir .. "/" .. file + local file_info = system.get_file_info(path) + if file_info and file_info.type == "file" then + ::read_instance_file:: + ---@type plugins.ipc.instance + local instance = dofile(path) + if instance and instance.id ~= self.id then + if instance.last_update + 2 > os.time() then + table.insert(instances, instance) + else + -- Delete expired instance session maybe result of a crash + os.remove(path) + end + elseif not instance and path ~= self.file then + --We retry reading the file since it was been modified + --by its owner instance. + goto read_instance_file + end + end + end + end + else + core.error("IPC Error: failed getting running instances (%s)", errmsg) + end + + local instances_count = #instances + + if instances_count > 0 then + table.sort(instances, function(ia, ib) + return ia.position < ib.position + end) + end + + if not self.primary and self.position then + if instances_count == 0 or instances[1].position > self.position then + self.primary = true + end + end + + return instances +end + +---@class plugins.ipc.vardecl +---@field name string +---@field type string +---@field optional boolean + +---Generate a string representation of a function +---@param name string +---@param params? plugins.ipc.vardecl[] +---@param returns? plugins.ipc.vardecl[] +---@return string function_definition +local function generate_definition(name, params, returns) + local declaration = name .. "(" + + if params and #params > 0 then + local params_string = "" + for _, param in ipairs(params) do + params_string = params_string .. param.name + if param.optional then + params_string = params_string .. "?: " + else + params_string = params_string .. ": " + end + params_string = params_string .. param.type .. ", " + end + local params_stripped = params_string:gsub(", $", "") + declaration = declaration .. params_stripped + end + + declaration = declaration .. ")" + + if returns and #returns > 0 then + declaration = declaration .. " -> " + local returns_string = "" + for _, ret in ipairs(returns) do + if ret.name then + returns_string = returns_string .. ret.name .. ": " + end + returns_string = returns_string .. ret.type + if ret.optional then + returns_string = returns_string .. "?, " + else + returns_string = returns_string .. ", " + end + end + local returns_stripped = returns_string:gsub(", $", "") + declaration = declaration .. returns_stripped + end + + return declaration +end + +---Retrieve the id of the primary instance if found. +---@return string | nil +function IPC:get_primary_instance() + local instances = self:get_instances() + for _, instance in ipairs(instances) do + if instance.primary then + return instance.id + end + end + return nil +end + +---Get a queued message. +---@param message_id string +---@return plugins.ipc.message | nil +function IPC:get_message(message_id) + for _, message in ipairs(self.messages) do + if message.id == message_id then + return message + end + end + return nil +end + +---Remove a message from the queue. +---@param message_id string +function IPC:remove_message(message_id) + for m, message in ipairs(self.messages) do + if message.id == message_id then + table.remove(self.messages, m) + break + end + end +end + +---Get the reply sent to a specific message. +---@param message_id string +---@return plugins.ipc.reply | nil +function IPC:get_reply(message_id) + for _, reply in ipairs(self.replies) do + if reply.id == message_id then + return reply + end + end + return nil +end + +---Verify all the messages sent by running instances, read those directed +---to the currently running instance and reply to them. +function IPC:read_messages() + local instances = self:get_instances() + + local awaiting_replies = {} + + for _, instance in ipairs(instances) do + for _, message in ipairs(instance.messages) do + for _, destination in ipairs(message.destinations) do + if destination == self.id then + local reply = self:get_reply(message.id) + + if not reply then + if message.on_read then + local on_read, errmsg = load(message.on_read) + if on_read then + local executed = core.try(function() on_read(message) end) + if not executed then + core.error( + "IPC Error: could not run message on_read\n" + .. "Message: %s\n", + common.serialize(message, {pretty = true}) + ) + end + else + core.error( + "IPC Error: could not run message on_read (%s)\n" + .. "Message: %s\n", + errmsg, + common.serialize(message, {pretty = true}) + ) + end + end + + ---@type plugins.ipc.reply + reply = {} + reply.id = message.id + reply.sender = message.sender + reply.replier = self.id + reply.data = {} + reply.on_read = nil + + local type_name = message.type .. "." .. message.name + + -- Allow listeners to react to message and modify reply + if self.listeners[type_name] and #self.listeners[type_name] > 0 then + for _, on_message in ipairs(self.listeners[type_name]) do + on_message(message, reply) + end + end + + if reply.on_read then + reply.on_read = string.dump(reply.on_read) + end + + reply.timestamp = os.time() + end + + table.insert(awaiting_replies, reply) + break + end + end + end + end + + self.replies = awaiting_replies +end + +---Reads replies directed to messages sent by the currently running instance +---and if any returns them. +---@return plugins.ipc.reply[] | nil +function IPC:read_replies() + if #self.messages == 0 then + return + end + + local instances = self:get_instances() + + local replies = {} + + local messages_removed = 0; + for m=1, #self.messages do + local message = self.messages[m-messages_removed] + local message_removed = false + + local destinations_removed = 0 + for d=1, #message.destinations do + local destination = message.destinations[d-destinations_removed] + + local found = false + for _, instance in ipairs(instances) do + if instance.id == destination then + found = true + for _, reply in ipairs(instance.replies) do + if reply.id == message.id then + local reply_registered = false + for _, message_reply in ipairs(message.replies) do + if message_reply.replier == instance.id then + reply_registered = true + break + end + end + if not reply_registered then + if message.on_reply then + message.on_reply(reply) + end + + if reply.on_read then + local on_read, errmsg = load(reply.on_read) + if on_read then + local executed = core.try(function() on_read(reply) end) + if not executed then + core.error( + "IPC Error: could not run reply on_read\n" + .. "Message: %s\n" + .. "Reply: %s", + common.serialize(message, {pretty = true}), + common.serialize(reply, {pretty = true}) + ) + end + else + core.error( + "IPC Error: could not run reply on_read (%s)\n" + .. "Message: %s\n" + .. "Reply: %s", + errmsg, + common.serialize(message, {pretty = true}), + common.serialize(reply, {pretty = true}) + ) + end + end + + table.insert(replies, reply) + table.insert(message.replies, reply) + end + end + end + break + end + end + if not found then + table.remove(message.destinations, d-destinations_removed) + destinations_removed = destinations_removed + 1 + if #message.destinations == 0 then + table.remove(self.messages, m-messages_removed) + messages_removed = messages_removed + 1 + message_removed = true + end + end + end + if + not message_removed + and + ( + #message.replies == #message.destinations + or + message.timestamp + MESSAGE_EXPIRATION < os.time() + ) + then + table.remove(self.messages, m-messages_removed) + messages_removed = messages_removed + 1 + end + end + + return replies +end + +---Blocks execution of current instance to wait for all replies by the +---specified message and when finished returns them. +---@param message_id string +---@return plugins.ipc.reply[] | nil +function IPC:wait_for_replies(message_id) + local message_data = self:get_message(message_id) + + update_file(self) + + if message_data then + self:read_replies() + while true do + if + message_data.replies + and + #message_data.replies == #message_data.destinations + then + return message_data.replies + elseif not self:get_message(message_id) then + return message_data.replies + end + self:read_replies() + end + end + return nil +end + +---Blocks execution of current instance to wait for all messages to +---be replied to. +function IPC:wait_for_messages() + update_file(self) + while #self.messages > 0 do + self:read_replies() + system.sleep(0.1) + end +end + +---@class plugins.ipc.sendmessageoptions +---@field data table<string,any> @Optional data given to the receiver. +---@field on_reply plugins.ipc.onreply @Callback that allows monitoring all the replies received for this message. +---@field on_read plugins.ipc.onmessage @Function executed by the message receiver. +---@field destinations string | table<integer,string> | nil @Id of the running instances to receive the message, if not set all running instances will receive the message. + +---Queue a new message to be sent to other lite-xl instances. +---@param name string +---@param options? plugins.ipc.sendmessageoptions +---@param message_type? plugins.ipc.messagetype +---@return string | nil message_id +function IPC:send_message(name, options, message_type) + options = options or {} + + local found_destinations = {} + local instances = self:get_instances() + local destinations = options.destinations + + if type(destinations) == "string" then + destinations = { destinations } + end + + if not destinations then + for _, instance in ipairs(instances) do + table.insert(found_destinations, instance.id) + end + else + for _, destination in ipairs(destinations) do + for _, instance in ipairs(instances) do + if instance.id == destination then + table.insert(found_destinations, destination) + end + end + end + end + + if #found_destinations <= 0 then + return nil + end + + ---@type plugins.ipc.message + local message = {} + message.id = self.id .. "." .. tostring(system.get_time()) + message.name = name + message.type = message_type or "message" + message.sender = self.id + message.data = options.data or {} + message.destinations = found_destinations + message.timestamp = os.time() + message.on_reply = options.on_reply or nil + message.on_read = options.on_read and string.dump(options.on_read) or nil + message.replies = {} + + table.insert(self.messages, message) + + update_file(self) + + return message.id +end + +---Add a listener for a given type of message. +---@param name string +---@param callback plugins.ipc.onmessage +---@param message_type? plugins.ipc.messagetype +---@return integer listener_position +function IPC:listen_message(name, callback, message_type) + message_type = message_type or "message" + + local type_name = message_type .. "." .. name + if not self.listeners[type_name] then + self.listeners[type_name] = {} + end + + table.insert(self.listeners[type_name], callback) + + return #self.listeners[type_name] +end + +---Listen for a given signal. +---@param name string +---@param callback plugins.ipc.function +---@return integer listener_position +function IPC:listen_signal(name, callback) + local signal_cb = function(message) + callback(table.unpack(message.data)) + end + return self:listen_message(name, signal_cb, "signal") +end + +---Add a new signal that can be sent to other instances. +---@param name string A unique name for the signal. +---@param params? plugins.ipc.vardecl[] Parameters that are going to be passed into callback. +function IPC:register_signal(name, params) + if self.signals[name] then + core.log_quiet("IPC: Overriding signal '%s'", name) + table.remove(self.signal_definitions, self.signals[name]) + end + + self.signals[name] = table.insert( + self.signal_definitions, + generate_definition(name, params) + ) + + table.sort(self.signal_definitions) +end + +---Add a new method that can be invoked from other instances. +---@param name string A unique name for the method. +---@param method fun(...) Function invoked when the method is called. +---@param params? plugins.ipc.vardecl[] Parameters that are going to be passed into method. +---@param returns? plugins.ipc.vardecl[] Return values of the method. +function IPC:register_method(name, method, params, returns) + if self.methods[name] then + core.log_quiet("IPC: Overriding method '%s'", name) + table.remove(self.method_definitions, self.methods[name]) + end + + self.methods[name] = table.insert( + self.method_definitions, + generate_definition(name, params, returns) + ) + + table.sort(self.method_definitions) + + self:listen_message(name, function(message, reply) + local ret = table.pack(method(table.unpack(message.data))) + reply.data = ret + end, "method") +end + +---Broadcast a signal to running instances. +---@param destinations string | table<integer, string> | nil +---@param name string +---@vararg any signal_parameters +function IPC:signal(destinations, name, ...) + self:send_message(name, { + destinations = destinations, + data = table.pack(self.id, ...) + }, "signal") +end + +---Call a method on another instance and wait for reply. +---@param destinations string | table<integer, string> | nil +---@param name string +---@return any | table<string,table> return_of_called_method +function IPC:call(destinations, name, ...) + local message_id = self:send_message(name, { + destinations = destinations, + data = table.pack(...) + }, "method") + + local ret = nil + + if message_id then + local replies = self:wait_for_replies(message_id) + if replies and #replies > 1 then + ret = {} + for _, reply in ipairs(replies) do + ret[reply.replier] = reply.data + end + elseif replies and #replies > 0 then + return table.unpack(replies[1].data) + end + else + core.error("IPC Error: could not make call to '%s'", name) + end + + return ret +end + +---Call a method on another instance asynchronously waiting for the replies. +---@param destinations string | table<integer, string> | nil +---@param name string +---@param callback fun(id: string, ret: table) | nil Called with the returned values +---@return string | nil message_id +function IPC:call_async(destinations, name, callback, ...) + return self:send_message(name, { + destinations = destinations, + data = table.pack(...), + on_reply = callback and function(reply) + callback(reply.replier, reply.data) + end or nil + }, "method") +end + +---Main ipc session for current instance. +---@type core.ipc +local ipc = IPC() + +---Get the IPC session for the running lite-xl instance. +---@return core.ipc +function IPC.current() + return ipc +end + +-------------------------------------------------------------------------------- +-- Override system.wait_event to allow ipc monitoring on the background. +-------------------------------------------------------------------------------- +local system_wait_event = system.wait_event + +local run_threads = coroutine.wrap(function() + while true do + for k, thread in pairs(threads) do + if thread.wake < system.get_time() then + local _, wait = assert(coroutine.resume(thread.cr)) + if coroutine.status(thread.cr) == "dead" then + table.remove(threads, k) + elseif wait then + thread.wake = system.get_time() + wait + end + end + coroutine.yield() + end + end +end) + +system.wait_event = function(timeout) + run_threads() + + if not timeout then + if not system.window_has_focus() then + local t = system.get_time() + local h = 0.5 / 2 + local dt = math.ceil(t / h) * h - t + + system_wait_event(dt + 1 / config.fps) + else + system_wait_event() + end + else + system_wait_event(timeout) + end +end + +-------------------------------------------------------------------------------- +-- Override system.show_fatal_error to be able and destroy session file on crash. +-------------------------------------------------------------------------------- +local system_show_fatal_error = system.show_fatal_error + +system.show_fatal_error = function(title, message) + if title == "Lite XL internal error" then + ipc:stop() + end + system_show_fatal_error(title, message) +end + +-------------------------------------------------------------------------------- +-- Override core.run to destroy ipc session file on exit. +-------------------------------------------------------------------------------- +local core_run = core.run + +core.run = function() + core_run() + ipc:stop() +end + +-------------------------------------------------------------------------------- +-- Override system.get_time temporarily as first function called on core.run +-- to allow settings gui to properly load ipc config options. +-------------------------------------------------------------------------------- +local system_get_time = system.get_time + +system.get_time = function() + if settings_found and not settings.ui then + return system_get_time() + end + + if config.plugins.ipc.single_instance then + system.get_time = system_get_time + + local primary_instance = ipc:get_primary_instance() + if primary_instance and ARGS[2] then + local open_directory = false + for i=2, #ARGS do + local path = system.absolute_path(ARGS[i]) + + if path then + local path_info = system.get_file_info(path) + if path_info then + if path_info.type == "file" then + ipc:call_async(primary_instance, "core.open_file", nil, path) + else + if config.plugins.ipc.dirs_instance == "add" then + ipc:call_async(primary_instance, "core.open_directory", nil, path) + elseif config.plugins.ipc.dirs_instance == "change" then + ipc:call_async(primary_instance, "core.change_directory", nil, path) + else + if #ARGS > 2 then + system.exec(string.format("%q %q", EXEFILE, path)) + else + open_directory = true + end + end + end + end + end + end + ipc:wait_for_messages() + if not open_directory then + os.exit() + end + end + else + system.get_time = system_get_time + end + + return system_get_time() +end + +-------------------------------------------------------------------------------- +-- Register methods for opening files and directories. +-------------------------------------------------------------------------------- +ipc:register_method("core.open_file", function(file) + if system.get_file_info(file) then + if system.raise_window then system.raise_window() end + core.root_view:open_doc(core.open_doc(file)) + end +end, {{name = "file", type = "string"}}) + +ipc:register_method("core.open_directory", function(directory) + if system.get_file_info(directory) then + if system.raise_window then system.raise_window() end + core.add_project_directory(directory) + end +end, {{name = "directory", type = "string"}}) + +ipc:register_method("core.change_directory", function(directory) + if system.get_file_info(directory) then + if system.raise_window then system.raise_window() end + if directory == core.project_dir then return end + core.confirm_close_docs(core.docs, function(dirpath) + core.open_folder_project(dirpath) + end, directory) + end +end, {{name = "directory", type = "string"}}) + +-------------------------------------------------------------------------------- +-- Register file dragging signals from instance to instance +-------------------------------------------------------------------------------- +ipc:register_signal("core.tab_drag_start", {{name = "file", type = "string"}}) +ipc:register_signal("core.tab_drag_stop") +ipc:register_signal("core.tab_drag_received", {{name = "file", type = "string"}}) + +local rootview_tab_dragging = false +local rootview_dragged_node = nil +local rootview_waiting_drop_file = "" +local rootview_waiting_drop_instance = "" + +local rootview_on_mouse_moved = RootView.on_mouse_moved +function RootView:on_mouse_moved(x, y, dx, dy) + rootview_on_mouse_moved(self, x, y, dx, dy) + if + self.dragged_node and self.dragged_node.dragging + and + not rootview_tab_dragging + then + ---@type core.doc + local doc = core.active_view.doc + if doc and doc.abs_filename then + rootview_tab_dragging = true + ipc:signal(nil, "core.tab_drag_start", doc.abs_filename) + rootview_dragged_node = self.dragged_node + end + elseif rootview_dragged_node then + local w, h, wx, wy = system.get_window_size() + if x < 0 or x > w or y < 0 or y > h then + self.dragged_node = nil + self:set_show_overlay(self.drag_overlay, false) + elseif not self.dragged_node then + self.dragged_node = rootview_dragged_node + self:set_show_overlay(self.drag_overlay, true) + end + core.request_cursor("hand") + elseif rootview_waiting_drop_file ~= "" then + ipc:signal( + rootview_waiting_drop_instance, + "core.tab_drag_received", + rootview_waiting_drop_file + ) + core.root_view:open_doc(core.open_doc(rootview_waiting_drop_file)) + rootview_waiting_drop_file = "" + end +end + +local rootview_on_mouse_released = RootView.on_mouse_released +function RootView:on_mouse_released(button, x, y, ...) + rootview_on_mouse_released(self, button, x, y, ...) + if rootview_tab_dragging then + rootview_tab_dragging = false + rootview_dragged_node = nil + ipc:signal(nil, "core.tab_drag_stop") + end +end + +ipc:listen_signal("core.tab_drag_start", function(instance, file) + rootview_waiting_drop_instance = instance + rootview_waiting_drop_file = file +end) + +ipc:listen_signal("core.tab_drag_stop", function() + rootview_waiting_drop_instance = "" + rootview_waiting_drop_file = "" +end) + +ipc:listen_signal("core.tab_drag_received", function() + command.perform("root:close") +end) + + +return IPC diff --git a/plugins/language_R.lua b/plugins/language_R.lua index ad3b483..afe3d1e 100644 --- a/plugins/language_R.lua +++ b/plugins/language_R.lua @@ -1,40 +1,39 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add{ - name = "R", - files = {"%.r$", "%.rds$", "%.rda$", "%.rdata$", "%.R$"}, - comment = "#", - patterns = { - {pattern = {"#", "\n"}, type = "comment"}, - {pattern = {'"', '"'}, type = "string"}, - {pattern = {"'", "'"}, type = "string"}, - {pattern = "[%a_][%w_]*%f[(]", type = "function"}, - {pattern = "[%a_][%w_]*", type = "symbol"}, - {pattern = "[%+%-=/%*%^%%<>!|&]", type = "operator"}, - {pattern = "0x[%da-fA-F]+", type = "number"}, - {pattern = "-?%d+[%d%.eE]*", type = "number"}, - {pattern = "-?%.?%d+", type = "number"}, - - }, - symbols = { - ["TRUE"] = "literal", - ["FALSE"] = "literal", - ["NA"] = "literal", - ["NULL"] = "literal", - ["Inf"] = "literal", - ["if"] = "keyword", - ["else"] = "keyword", - ["while"] = "keyword", - ["function"] = "keyword", - ["break"] = "keyword", - ["next"] = "keyword", - ["repeat"] = "keyword", - ["in"] = "keyword", - ["for"] = "keyword", - ["NA_integer"] = "keyword", - ["NA_complex"] = "keyword", - ["NA_character"] = "keyword", - ["NA_real"] = "keyword" - } + name = "R", + files = {"%.r$", "%.rds$", "%.rda$", "%.rdata$", "%.R$"}, + comment = "#", + patterns = { + {pattern = {"#", "\n"}, type = "comment"}, + {pattern = {'"', '"'}, type = "string"}, + {pattern = {"'", "'"}, type = "string"}, + {pattern = "[%a_][%w_]*%f[(]", type = "function"}, + {pattern = "[%a_][%w_]*", type = "symbol"}, + {pattern = "[%+%-=/%*%^%%<>!|&]", type = "operator"}, + {pattern = "0x[%da-fA-F]+", type = "number"}, + {pattern = "-?%d+[%d%.eE]*", type = "number"}, + {pattern = "-?%.?%d+", type = "number"}, + }, + symbols = { + ["TRUE"] = "literal", + ["FALSE"] = "literal", + ["NA"] = "literal", + ["NULL"] = "literal", + ["Inf"] = "literal", + ["if"] = "keyword", + ["else"] = "keyword", + ["while"] = "keyword", + ["function"] = "keyword", + ["break"] = "keyword", + ["next"] = "keyword", + ["repeat"] = "keyword", + ["in"] = "keyword", + ["for"] = "keyword", + ["NA_integer"] = "keyword", + ["NA_complex"] = "keyword", + ["NA_character"] = "keyword", + ["NA_real"] = "keyword" + } } diff --git a/plugins/language_angelscript.lua b/plugins/language_angelscript.lua index e62c1da..4e003ea 100644 --- a/plugins/language_angelscript.lua +++ b/plugins/language_angelscript.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_assembly_x86.lua b/plugins/language_assembly_x86.lua index baae3c4..e6d218b 100644 --- a/plugins/language_assembly_x86.lua +++ b/plugins/language_assembly_x86.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 -- Support for the intel syntax x86-64 assembly -- Simply add it to lite-xl's plugins folder (located somewhere at home/.config/lite-xl/plugins) -- https://github.com/DMClVG diff --git a/plugins/language_batch.lua b/plugins/language_batch.lua index 13753cf..32510c8 100644 --- a/plugins/language_batch.lua +++ b/plugins/language_batch.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" -- batch syntax for lite <liqube> diff --git a/plugins/language_bib.lua b/plugins/language_bib.lua index cfde8da..1850ec6 100644 --- a/plugins/language_bib.lua +++ b/plugins/language_bib.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_caddyfile.lua b/plugins/language_caddyfile.lua new file mode 100644 index 0000000..f918c16 --- /dev/null +++ b/plugins/language_caddyfile.lua @@ -0,0 +1,104 @@ +-- mod-version:3 +local syntax = require "core.syntax" + +syntax.add { + files = { "Caddyfile" }, + comment = "#", + patterns = { + { pattern = { "#", "\n"}, type = "comment" }, + { pattern = { '"', '"', '\\' }, type = "string" }, + -- Matcher definition + { pattern = "@[%w_]+", type = "operator" }, + -- Snippet + { pattern = "%(%g+%)", type = "operator" }, + -- Properties + { pattern = "^[%a_][%w_]*()%s+%f[%g]", + type = { "function", "normal" } + }, + { pattern = "^[%a_][%w_]*()%s+$", + type = { "function", "normal" } + }, + { pattern = "^%s*()[%a_][%w_]*()%s+$", + type = { "normal", "function", "normal" } + }, + { pattern = "^%s*()[%a_][%w_]*()%s+%f[%g]", + type = { "normal", "function", "normal" } + }, + -- Environment variables + { pattern = "{()%$[%w_]+():()[%w_]+()}", + type = { "operator", "keyword2", "operator", "keyword2", "operator" } + }, + { pattern = "{()%$[%w_]+()}", + type = { "operator", "keyword2", "operator" } + }, + -- Place holder + { pattern = "{%g-}", type = "keyword2" }, + -- Operators + { pattern = "[+%-,:]", type = "operator" }, + -- IP Address + { pattern = "%d+%.%d+%.%d+%.%d+", type = "literal" }, + -- Path /path/subpath + { pattern = "/[%w%./]+", type = "literal" }, + -- Wildcard domain *.levels + { pattern = "%*()[%w.]+", + type = { "operator", "literal" } + }, + -- Match Operator + { pattern = "%*+", type = "operator" }, + -- Domain leve1.level2 + { pattern = "https?://[%w%./%*]+", type = "literal" }, + -- Domain leve1.level2 + { pattern = "%w+%.[%w%.]+", type = "literal" }, + -- Number + { pattern = "%d+[mhskbi]*", type = "number" }, + -- Everything else for symbols to work + { pattern = "[%a_][%w_]*", type = "symbol" }, + }, + symbols = { + ["true"] = "literal", + ["false"] = "literal", + ["localhost"] = "literal", + + -- built-in directives + ["abort"] = "keyword", + ["acme_server"] = "keyword", + ["basicauth"] = "keyword", + ["bind"] = "keyword", + ["encode"] = "keyword", + ["error"] = "keyword", + ["file_server"] = "keyword", + ["forward_auth"] = "keyword", + ["handle"] = "keyword", + ["handle_errors"] = "keyword", + ["handle_path"] = "keyword", + ["header"] = "keyword", + ["import"] = "keyword", + ["log"] = "keyword", + ["method"] = "keyword", + ["map"] = "keyword", + ["metrics"] = "keyword", + ["php_fastcgi"] = "keyword", + ["push"] = "keyword", + ["redir"] = "keyword", + ["request_body"] = "keyword", + ["request_header"] = "keyword", + ["respond"] = "keyword", + ["reverse_proxy"] = "keyword", + ["rewrite"] = "keyword", + ["root"] = "keyword", + ["route"] = "keyword", + ["templates"] = "keyword", + ["tls"] = "keyword", + ["tracing"] = "keyword", + ["try_files"] = "keyword", + ["uri"] = "keyword", + ["vars"] = "keyword", + + -- Module directives + ["cgi"] = "keyword", + ["ssh"] = "keyword", + ["exec"] = "keyword", + ["supervisor"] = "keyword", + ["layer4"] = "keyword", + }, +} diff --git a/plugins/language_cmake.lua b/plugins/language_cmake.lua index 8103632..19f1aa5 100644 --- a/plugins/language_cmake.lua +++ b/plugins/language_cmake.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_cpp.lua b/plugins/language_cpp.lua deleted file mode 100644 index b1afa0f..0000000 --- a/plugins/language_cpp.lua +++ /dev/null @@ -1,203 +0,0 @@ --- mod-version:2 -- lite-xl 2.0 -local syntax = require "core.syntax" - -syntax.add { - name = "C++", - files = { - "%.h$", "%.inl$", "%.cpp$", "%.cc$", "%.C$", "%.cxx$", - "%.c++$", "%.hh$", "%.H$", "%.hxx$", "%.hpp$", "%.h++$" - }, - comment = "//", - block_comment = { "/*", "*/" }, - patterns = { - { pattern = "//.-\n", type = "comment" }, - { pattern = { "/%*", "%*/" }, type = "comment" }, - { pattern = { '"', '"', '\\' }, type = "string" }, - { pattern = { "'", "'", '\\' }, type = "string" }, - { pattern = "0x%x+", type = "number" }, - { pattern = "%d+[%d%.'eE]*f?", type = "number" }, - { pattern = "%.?%d+f?", type = "number" }, - { pattern = "[%+%-=/%*%^%%<>!~|:&]", type = "operator" }, - { pattern = "##", type = "operator" }, - { pattern = "struct%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, - { pattern = "class%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, - { pattern = "union%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, - { pattern = "namespace%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, - -- static declarations - { pattern = "static()%s+()inline", - type = { "keyword", "normal", "keyword" } - }, - { pattern = "static()%s+()const", - type = { "keyword", "normal", "keyword" } - }, - { pattern = "static()%s+()[%a_][%w_]*", - type = { "keyword", "normal", "literal" } - }, - -- match method type declarations - { pattern = "[%a_][%w_]*()%s*()%**()%s*()[%a_][%w_]*()%s*()::", - type = { - "literal", "normal", "operator", "normal", - "literal", "normal", "operator" - } - }, - -- match function type declarations - { pattern = "[%a_][%w_]*()%*+()%s+()[%a_][%w_]*%f[%(]", - type = { "literal", "operator", "normal", "function" } - }, - { pattern = "[%a_][%w_]*()%s+()%*+()[%a_][%w_]*%f[%(]", - type = { "literal", "normal", "operator", "function" } - }, - { pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*%f[%(]", - type = { "literal", "normal", "function" } - }, - -- match variable type declarations - { pattern = "[%a_][%w_]*()%*+()%s+()[%a_][%w_]*", - type = { "literal", "operator", "normal", "normal" } - }, - { pattern = "[%a_][%w_]*()%s+()%*+()[%a_][%w_]*", - type = { "literal", "normal", "operator", "normal" } - }, - { pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*()%s*()[;,%[%)]", - type = { "literal", "normal", "normal", "normal", "normal" } - }, - { pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*()%s*()=", - type = { "literal", "normal", "normal", "normal", "operator" } - }, - { pattern = "[%a_][%w_]*()&()%s+()[%a_][%w_]*", - type = { "literal", "operator", "normal", "normal" } - }, - { pattern = "[%a_][%w_]*()%s+()&()[%a_][%w_]*", - type = { "literal", "normal", "operator", "normal" } - }, - -- Match scope operator element access - { pattern = "[%a_][%w_]*()%s*()::", - type = { "literal", "normal", "operator" } - }, - -- Uppercase constants of at least 2 chars in len - { pattern = "_?%u[%u_][%u%d_]*%f[%s%+%*%-%.%)%]}%?%^%%=/<>~|&;:,!]", - type = "number" - }, - -- Magic constants - { pattern = "__[%u%l]+__", type = "number" }, - -- all other functions - { pattern = "[%a_][%w_]*%f[(]", type = "function" }, - -- Macros - { pattern = "^%s*#%s*define%s+()[%a_][%a%d_]*", - type = { "keyword", "symbol" } - }, - { pattern = "#%s*include%s+()<.->", - type = { "keyword", "string" } - }, - { pattern = "%f[#]#%s*[%a_][%w_]*", type = "keyword" }, - -- Everything else to make the tokenizer work properly - { pattern = "[%a_][%w_]*", type = "symbol" }, - }, - symbols = { - ["alignof"] = "keyword", - ["alignas"] = "keyword", - ["and"] = "keyword", - ["and_eq"] = "keyword", - ["not"] = "keyword", - ["not_eq"] = "keyword", - ["or"] = "keyword", - ["or_eq"] = "keyword", - ["xor"] = "keyword", - ["xor_eq"] = "keyword", - ["private"] = "keyword", - ["protected"] = "keyword", - ["public"] = "keyword", - ["register"] = "keyword", - ["nullptr"] = "keyword", - ["operator"] = "keyword", - ["asm"] = "keyword", - ["bitand"] = "keyword", - ["bitor"] = "keyword", - ["catch"] = "keyword", - ["throw"] = "keyword", - ["try"] = "keyword", - ["class"] = "keyword", - ["compl"] = "keyword", - ["explicit"] = "keyword", - ["export"] = "keyword", - ["concept"] = "keyword", - ["consteval"] = "keyword", - ["constexpr"] = "keyword", - ["constinit"] = "keyword", - ["const_cast"] = "keyword", - ["dynamic_cast"] = "keyword", - ["reinterpret_cast"] = "keyword", - ["static_cast"] = "keyword", - ["static_assert"] = "keyword", - ["template"] = "keyword", - ["this"] = "keyword", - ["thread_local"] = "keyword", - ["requires"] = "keyword", - ["co_wait"] = "keyword", - ["co_return"] = "keyword", - ["co_yield"] = "keyword", - ["decltype"] = "keyword", - ["delete"] = "keyword", - ["friend"] = "keyword", - ["typeid"] = "keyword", - ["typename"] = "keyword", - ["mutable"] = "keyword", - ["override"] = "keyword", - ["virtual"] = "keyword", - ["using"] = "keyword", - ["namespace"] = "keyword", - ["new"] = "keyword", - ["noexcept"] = "keyword", - ["if"] = "keyword", - ["then"] = "keyword", - ["else"] = "keyword", - ["elseif"] = "keyword", - ["do"] = "keyword", - ["while"] = "keyword", - ["for"] = "keyword", - ["break"] = "keyword", - ["continue"] = "keyword", - ["return"] = "keyword", - ["goto"] = "keyword", - ["struct"] = "keyword", - ["union"] = "keyword", - ["typedef"] = "keyword", - ["enum"] = "keyword", - ["extern"] = "keyword", - ["static"] = "keyword", - ["volatile"] = "keyword", - ["const"] = "keyword", - ["inline"] = "keyword", - ["switch"] = "keyword", - ["case"] = "keyword", - ["default"] = "keyword", - ["auto"] = "keyword", - ["void"] = "keyword2", - ["int"] = "keyword2", - ["short"] = "keyword2", - ["long"] = "keyword2", - ["float"] = "keyword2", - ["double"] = "keyword2", - ["char"] = "keyword2", - ["unsigned"] = "keyword2", - ["bool"] = "keyword2", - ["true"] = "literal", - ["false"] = "literal", - ["NULL"] = "literal", - ["wchar_t"] = "keyword2", - ["char8_t"] = "keyword2", - ["char16_t"] = "keyword2", - ["char32_t"] = "keyword2", - ["#include"] = "keyword", - ["#if"] = "keyword", - ["#ifdef"] = "keyword", - ["#ifndef"] = "keyword", - ["#elif"] = "keyword", - ["#else"] = "keyword", - ["#elseif"] = "keyword", - ["#endif"] = "keyword", - ["#define"] = "keyword", - ["#warning"] = "keyword", - ["#error"] = "keyword", - ["#pragma"] = "keyword", - }, -} diff --git a/plugins/language_csharp.lua b/plugins/language_csharp.lua index 5e1e81a..c137b63 100644 --- a/plugins/language_csharp.lua +++ b/plugins/language_csharp.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_d.lua b/plugins/language_d.lua index e59916e..6788f0b 100644 --- a/plugins/language_d.lua +++ b/plugins/language_d.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_dart.lua b/plugins/language_dart.lua index 97aa375..03274b2 100644 --- a/plugins/language_dart.lua +++ b/plugins/language_dart.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_diff.lua b/plugins/language_diff.lua index 4376b26..c4c5a90 100644 --- a/plugins/language_diff.lua +++ b/plugins/language_diff.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" local style = require "core.style" local common = require "core.common" diff --git a/plugins/language_elixir.lua b/plugins/language_elixir.lua index f414bd4..8f47770 100644 --- a/plugins/language_elixir.lua +++ b/plugins/language_elixir.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_elm.lua b/plugins/language_elm.lua index 65ddc1f..2d33813 100644 --- a/plugins/language_elm.lua +++ b/plugins/language_elm.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_erb.lua b/plugins/language_erb.lua index e63c7b0..d55b48c 100644 --- a/plugins/language_erb.lua +++ b/plugins/language_erb.lua @@ -1,4 +1,4 @@ --- mod-version:2 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_fe.lua b/plugins/language_fe.lua index 18400ac..aee9b85 100644 --- a/plugins/language_fe.lua +++ b/plugins/language_fe.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_fennel.lua b/plugins/language_fennel.lua index d2fa7f0..dcfd245 100644 --- a/plugins/language_fennel.lua +++ b/plugins/language_fennel.lua @@ -1,102 +1,268 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 -- Support for the Fennel programming language: https://fennel-lang.org --- Covers all the keywords up to Fennel version 0.4.0 +-- Covers all the keywords up to Fennel version 1.2.0 -- Currently only covers highlighting, not indentation, delimiter -- matching, or evaluation. -local syntax = require "core.syntax" +local syntax = require("core.syntax") -syntax.add { - name = "Fennel", - files = "%.fnl$", - comment = ";", - patterns = { - { pattern = ";.-\n", type = "comment" }, - { pattern = { '"', '"', '\\' }, type = "string" }, - { pattern = "0x[%da-fA-F]+", type = "number" }, - { pattern = "-?%d+[%d%.]*", type = "number" }, - { pattern = "-?%.?%d+", type = "number" }, - { pattern = "%f[^(][^()'%s\"]+", type = "function" }, - { pattern = "[^()'%s\"]+", type = "symbol" }, - }, +syntax.add({ + comment = ";", + files = "%.fnl$", + name = "Fennel", + patterns = { + { pattern = ";.-\n", type = "comment" }, + { pattern = { '"', '"', "\\" }, type = "string" }, + { pattern = "0x[%da-fA-F]+", type = "number" }, + { pattern = "-?%d+[%d%.]*", type = "number" }, + { pattern = "-?%.?%d+", type = "number" }, + { pattern = "%f[^(][^()'%s\"]+", type = "function" }, + { pattern = "[^()'%s\"]+", type = "symbol" }, + }, + symbols = { + ["#"] = "keyword", + ["%"] = "keyword", + ["*"] = "keyword", + ["+"] = "keyword", + ["-"] = "keyword", + ["->"] = "keyword", + ["->>"] = "keyword", + ["-?>"] = "keyword", + ["-?>>"] = "keyword", + ["."] = "keyword", + [".."] = "keyword", + ["/"] = "keyword", + ["//"] = "keyword", + [":"] = "keyword", + ["<"] = "keyword", + ["<="] = "keyword", + ["="] = "keyword", + [">"] = "keyword", + [">="] = "keyword", + ["?."] = "keyword", + ["^"] = "keyword", + _G = "keyword", + accumulate = "keyword2", + ["and"] = "keyword2", + arg = "keyword2", + assert = "keyword2", + band = "keyword2", + bit32 = "keyword2", + ["bit32.arshift"] = "keyword2", + ["bit32.band"] = "keyword2", + ["bit32.bnot"] = "keyword2", + ["bit32.bor"] = "keyword2", + ["bit32.btest"] = "keyword2", + ["bit32.bxor"] = "keyword2", + ["bit32.extract"] = "keyword2", + ["bit32.lrotate"] = "keyword2", + ["bit32.lshift"] = "keyword2", + ["bit32.replace"] = "keyword2", + ["bit32.rrotate"] = "keyword2", + ["bit32.rshift"] = "keyword2", + bnot = "keyword2", + bor = "keyword2", + bxor = "keyword2", + collect = "keyword2", + collectgarbage = "keyword2", + comment = "keyword2", + coroutine = "keyword2", + ["coroutine.create"] = "keyword2", + ["coroutine.resume"] = "keyword2", + ["coroutine.running"] = "keyword2", + ["coroutine.status"] = "keyword2", + ["coroutine.wrap"] = "keyword2", + ["coroutine.yield"] = "keyword2", + debug = "keyword2", + ["debug.debug"] = "keyword2", + ["debug.gethook"] = "keyword2", + ["debug.getinfo"] = "keyword2", + ["debug.getlocal"] = "keyword2", + ["debug.getmetatable"] = "keyword2", + ["debug.getregistry"] = "keyword2", + ["debug.getupvalue"] = "keyword2", + ["debug.getuservalue"] = "keyword2", + ["debug.sethook"] = "keyword2", + ["debug.setlocal"] = "keyword2", + ["debug.setmetatable"] = "keyword2", + ["debug.setupvalue"] = "keyword2", + ["debug.setuservalue"] = "keyword2", + ["debug.traceback"] = "keyword2", + ["debug.upvalueid"] = "keyword2", + ["debug.upvaluejoin"] = "keyword2", + ["do"] = "keyword2", + dofile = "keyword2", + doto = "keyword2", + each = "keyword2", + error = "keyword2", + ["eval-compiler"] = "keyword2", + ["false"] = "literal", + fcollect = "keyword2", + fn = "keyword2", + ["for"] = "keyword2", + getmetatable = "keyword2", + global = "keyword2", + hashfn = "keyword2", + icollect = "keyword2", + ["if"] = "keyword2", + ["import-macros"] = "keyword2", + include = "keyword2", + io = "keyword2", + ["io.close"] = "keyword2", + ["io.flush"] = "keyword2", + ["io.input"] = "keyword2", + ["io.lines"] = "keyword2", + ["io.open"] = "keyword2", + ["io.output"] = "keyword2", + ["io.popen"] = "keyword2", + ["io.read"] = "keyword2", + ["io.tmpfile"] = "keyword2", + ["io.type"] = "keyword2", + ["io.write"] = "keyword2", + ipairs = "keyword2", + lambda = "keyword2", + length = "keyword2", + let = "keyword2", + load = "keyword2", + loadfile = "keyword2", + loadstring = "keyword2", + ["local"] = "keyword2", + lshift = "keyword2", + lua = "keyword2", + macro = "keyword2", + macrodebug = "keyword2", + macros = "keyword2", + match = "keyword2", + ["match-try"] = "keyword2", + math = "keyword2", + ["math.abs"] = "keyword2", + ["math.acos"] = "keyword2", + ["math.asin"] = "keyword2", + ["math.atan"] = "keyword2", + ["math.atan2"] = "keyword2", + ["math.ceil"] = "keyword2", + ["math.cos"] = "keyword2", + ["math.cosh"] = "keyword2", + ["math.deg"] = "keyword2", + ["math.exp"] = "keyword2", + ["math.floor"] = "keyword2", + ["math.fmod"] = "keyword2", + ["math.frexp"] = "keyword2", + ["math.ldexp"] = "keyword2", + ["math.log"] = "keyword2", + ["math.log10"] = "keyword2", + ["math.max"] = "keyword2", + ["math.min"] = "keyword2", + ["math.modf"] = "keyword2", + ["math.pow"] = "keyword2", + ["math.rad"] = "keyword2", + ["math.random"] = "keyword2", + ["math.randomseed"] = "keyword2", + ["math.sin"] = "keyword2", + ["math.sinh"] = "keyword2", + ["math.sqrt"] = "keyword2", + ["math.tan"] = "keyword2", + ["math.tanh"] = "keyword2", + module = "keyword2", + next = "keyword2", + ["nil"] = "literal", + ["not"] = "keyword2", + ["not="] = "keyword2", + ["or"] = "keyword2", + os = "keyword2", + ["os.clock"] = "keyword2", + ["os.date"] = "keyword2", + ["os.difftime"] = "keyword2", + ["os.execute"] = "keyword2", + ["os.exit"] = "keyword2", + ["os.getenv"] = "keyword2", + ["os.remove"] = "keyword2", + ["os.rename"] = "keyword2", + ["os.setlocale"] = "keyword2", + ["os.time"] = "keyword2", + ["os.tmpname"] = "keyword2", + package = "keyword2", + ["package.loadlib"] = "keyword2", + ["package.searchpath"] = "keyword2", + ["package.seeall"] = "keyword2", + pairs = "keyword2", + partial = "keyword2", + pcall = "keyword2", + ["pick-args"] = "keyword2", + ["pick-values"] = "keyword2", + print = "keyword2", + quote = "keyword2", + rawequal = "keyword2", + rawget = "keyword2", + rawlen = "keyword2", + rawset = "keyword2", + require = "keyword2", + ["require-macros"] = "keyword2", + rshift = "keyword2", + select = "keyword2", + set = "keyword2", + ["set-forcibly!"] = "keyword2", + setmetatable = "keyword2", + string = "keyword2", + ["string.byte"] = "keyword2", + ["string.char"] = "keyword2", + ["string.dump"] = "keyword2", + ["string.find"] = "keyword2", + ["string.format"] = "keyword2", + ["string.gmatch"] = "keyword2", + ["string.gsub"] = "keyword2", + ["string.len"] = "keyword2", + ["string.lower"] = "keyword2", + ["string.match"] = "keyword2", + ["string.rep"] = "keyword2", + ["string.reverse"] = "keyword2", + ["string.sub"] = "keyword2", + ["string.upper"] = "keyword2", + table = "keyword2", + ["table.concat"] = "keyword2", + ["table.insert"] = "keyword2", + ["table.maxn"] = "keyword2", + ["table.pack"] = "keyword2", + ["table.remove"] = "keyword2", + ["table.sort"] = "keyword2", + ["table.unpack"] = "keyword2", + tonumber = "keyword2", + tostring = "keyword2", + ["true"] = "literal", + tset = "keyword2", + type = "keyword2", + unpack = "keyword2", + values = "keyword2", + var = "keyword2", + when = "keyword2", + ["while"] = "keyword2", + ["with-open"] = "keyword2", + xpcall = "keyword2", + ["~="] = "keyword", + ["λ"] = "keyword", + }, +}) - symbols = { - ["eval-compiler"] = "keyword2", - ["doc"] = "keyword2", - ["lua"] = "keyword2", - ["hashfn"] = "keyword2", - ["macro"] = "keyword2", - ["macros"] = "keyword2", - ["import-macros"] = "keyword2", - ["do"] = "keyword2", - ["values"] = "keyword2", - ["if"] = "keyword2", - ["when"] = "keyword2", - ["each"] = "keyword2", - ["for"] = "keyword2", - ["fn"] = "keyword2", - ["lambda"] = "keyword2", - ["λ"] = "keyword2", - ["partial"] = "keyword2", - ["while"] = "keyword2", - ["set"] = "keyword2", - ["global"] = "keyword2", - ["var"] = "keyword2", - ["local"] = "keyword2", - ["let"] = "keyword2", - ["tset"] = "keyword2", - ["set-forcibly!"] = "keyword2", - ["doto"] = "keyword2", - ["match"] = "keyword2", - ["or"] = "keyword2", - ["and"] = "keyword2", - ["not"] = "keyword2", - ["not="] = "keyword2", - ["pick-args"] = "keyword2", - ["pick-values"] = "keyword2", - ["macrodebug"] = "keyword2", +-- To regenerate the syntax from the compiler: +-- (macro s [] +-- (let [{: syntax} (require :fennel) +-- symbols {:nil :literal +-- :true :literal +-- :false :literal}] +-- `(syntax.add {:name "Fennel" +-- :files "%.fnl$" +-- :comment ";" +-- :patterns [{:type :comment :pattern ";.-\n"} +-- {:type :string :pattern {1 "\"" 2 "\"" 3 "\\"}} +-- {:type :number :pattern "0x[%da-fA-F]+"} +-- {:type :number :pattern "-?%d+[%d%.]*"} +-- {:type :number :pattern "-?%.?%d+"} +-- {:type :function :pattern "%f[^(][^()'%s\"]+"} +-- {:type :symbol :pattern "[^()'%s\"]+"}] +-- :symbols ,(collect [name (pairs (syntax)) :into symbols] +-- (values name +-- (if (name:find "[a-z]") +-- :keyword2 :keyword)))}))) (s) - ["."] = "keyword", - ["+"] = "keyword", - [".."] = "keyword", - ["^"] = "keyword", - ["-"] = "keyword", - ["*"] = "keyword", - ["%"] = "keyword", - ["/"] = "keyword", - [">"] = "keyword", - ["<"] = "keyword", - [">="] = "keyword", - ["<="] = "keyword", - ["="] = "keyword", - ["#"] = "keyword", - ["..."] = "keyword", - [":"] = "keyword", - ["->"] = "keyword", - ["->>"] = "keyword", - ["-?>"] = "keyword", - ["-?>>"] = "keyword", - ["$"] = "keyword", - ["$1"] = "keyword", - ["$2"] = "keyword", - ["$3"] = "keyword", - ["$4"] = "keyword", - ["$5"] = "keyword", - ["$6"] = "keyword", - ["$7"] = "keyword", - ["$8"] = "keyword", - ["$9"] = "keyword", - - ["lshift"] = "keyword", - ["rshift"] = "keyword", - ["bor"] = "keyword", - ["band"] = "keyword", - ["bnot"] = "keyword", - ["bxor"] = "keyword", - - ["nil"] = "literal", - ["true"] = "literal", - ["false"] = "literal", - } -} +-- and reformat the output, of course diff --git a/plugins/language_fstab.lua b/plugins/language_fstab.lua index 4198516..bbe418e 100644 --- a/plugins/language_fstab.lua +++ b/plugins/language_fstab.lua @@ -1,4 +1,4 @@ --- mod-version:2 +-- mod-version:3 local syntax = require "core.syntax" @@ -7,7 +7,7 @@ syntax.add { files = { "fstab" }, comment = '#', patterns = { - -- Only lines that start with a # are comments; you can have #'s in fuse + -- Only lines that start with a # are comments; you can have #'s in fuse -- filesystem strings that aren't comments, so shouldn't be highlighted as such. { regex = "^#.*$", type = "comment" }, { pattern = "[=/:.,]+", type = "operator" }, @@ -45,7 +45,7 @@ syntax.add { ["LABEL"] = "keyword", ["UUID"] = "keyword", - + -- filesystems ["aufs"] = "keyword2", ["autofs"] = "keyword2", diff --git a/plugins/language_gdscript.lua b/plugins/language_gdscript.lua index 168fc44..d03dce8 100644 --- a/plugins/language_gdscript.lua +++ b/plugins/language_gdscript.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 -- Support for the GDScript programming language: https://godotengine.org/ -- Covers the most used keywords up to Godot version 3.2.x diff --git a/plugins/language_glsl.lua b/plugins/language_glsl.lua index f8b2782..360e11f 100644 --- a/plugins/language_glsl.lua +++ b/plugins/language_glsl.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local style = require "core.style" local common = require "core.common" diff --git a/plugins/language_gmi.lua b/plugins/language_gmi.lua index a6ef4d8..c5d819a 100644 --- a/plugins/language_gmi.lua +++ b/plugins/language_gmi.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" @@ -10,7 +10,7 @@ syntax.add { { pattern = { "```", "```" }, type = "string" }, { pattern = "#.*", type = "keyword" }, { pattern = "%*%s", type = "keyword2" }, - { pattern = "=>", type = "function" }, + { pattern = "=>", type = "function" }, { pattern = "https?://%S+", type = "literal" }, { pattern = "gemini?://%S+", type = "literal" }, { pattern = ">.*", type = "comment" }, diff --git a/plugins/language_go.lua b/plugins/language_go.lua index 58c38c2..93db0e4 100644 --- a/plugins/language_go.lua +++ b/plugins/language_go.lua @@ -1,26 +1,143 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { name = "Go", files = { "%.go$" }, comment = "//", + block_comment = {"/*", "*/"}, patterns = { - { pattern = "//.-\n", type = "comment" }, - { pattern = { "/%*", "%*/" }, type = "comment" }, - { pattern = { '"', '"', '\\' }, type = "string" }, - { pattern = { "`", "`", '\\' }, type = "string" }, - { pattern = { "'", "'", '\\' }, type = "string" }, - { pattern = "0[oO_][0-7]+", type = "number" }, - { pattern = "-?0x[%x_]+", type = "number" }, - { pattern = "-?%d+_%d", type = "number" }, - { pattern = "-?%d+[%d%.eE]*f?", type = "number" }, - { pattern = "-?%.?%d+f?", type = "number" }, - { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" }, - { pattern = ":=", type = "operator" }, - { pattern = "[%a_][%w_]*%f[(]", type = "function" }, -- function call - { pattern = "func()%s*[%a_][%w_]*()%f[%[(]", type = {"keyword", "function", "normal"} }, -- function statement - { pattern = "[%a_][%w_]*", type = "symbol" }, + { pattern = "//.-\n", type = "comment" }, + { pattern = { "/%*", "%*/" }, type = "comment" }, + { pattern = { '"', '"', '\\' }, type = "string" }, + { pattern = { "`", "`", '\\' }, type = "string" }, + { pattern = { "'", "'", '\\' }, type = "string" }, + { pattern = "0[oO_][0-7]+i?", type = "number" }, + { pattern = "-?0x[%x_]+i?", type = "number" }, + { pattern = "-?%d+_%di?", type = "number" }, + { pattern = "-?%d+[%d%.eE]*f?i?", type = "number" }, + { pattern = "-?%.?%d+f?i?", type = "number" }, + -- goto label + { pattern = "^%s+()[%a_][%w%_]*()%s*:%s$", -- this is to fix `default:` + type = { "normal", "function", "normal" } + }, + { pattern = "^%s*[%a_][%w%_]*()%s*:%s$", + type = { "function", "normal" } + }, + -- pointer, generic and reference type + { pattern = "[%*~&]()[%a_][%w%_]*", + type = { "operator", "keyword2" } + }, + -- slice type + { pattern = "%[%]()[%a_][%w%_]*", + type = { "operator", "keyword2" } + }, + -- type coerce + { + pattern = "%.%(()[%a_][%w_]*()%)", + type = { "normal", "keyword2", "normal" } + }, + -- struct literal + { pattern = "[%a_][%w%_]*()%s*{%s*", + type = { "keyword2", "normal" } + }, + -- operators + { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" }, + { pattern = ":=", type = "operator" }, + -- function calls + { pattern = "func()%s*[%a_][%w_]*()%f[%[(]", -- function statement + type = {"keyword", "function", "normal"} + }, + { pattern = "[%a_][%w_]*%f[(]", type = "function" }, + { pattern = "%.()[%a_][%w_]*%f[(]", + type = { "normal", "function" } + }, + -- type declaration + { pattern = "type()%s+()[%a_][%w%_]*", + type = { "keyword", "normal", "keyword2" } + }, + -- variable declaration + { pattern = "var()%s+()[%a_][%w%_]*", + type = { "keyword", "normal", "symbol" } + }, + -- goto + { pattern = "goto()%s+()[%a_][%w%_]*", + type = { "keyword", "normal", "function" } + }, + -- if fix + { pattern = "if()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- for fix + { pattern = "for()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- return fix + { pattern = "return()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- range fix + { pattern = "range()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- func fix + { pattern = "func()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- switch fix + { pattern = "switch()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- case fix + { pattern = "case()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- break fix + { pattern = "break()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- continue fix + { pattern = "continue()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- package fix + { pattern = "package()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- go fix + { pattern = "go()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- chan fix + { pattern = "chan()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- defer fix + { pattern = "defer()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- field declaration + { pattern = "[%a_][%w%_]*()%s*():%s*%f[%w%p]", + type = { "function", "normal", "operator" } + }, + -- parameters or declarations + { pattern = "[%a_][%w%_]*()%s+()[%*~&]?()[%a_][%w%_]*", + type = { "literal", "normal", "operator", "keyword2" } + }, + { pattern = "[%a_][%w_]*()%s+()%[%]()[%a_][%w%_]*", + type = { "literal", "normal", "normal", "keyword2" } + }, + -- single return type + { + pattern = "%)%s+%(?()[%a_][%w%_]*()%)?%s+%{", + type = { "normal", "keyword2", "normal" } + }, + -- sub fields + { pattern = "%.()[%a_][%w_]*", + type = { "normal", "literal" } + }, + -- every other symbol + { pattern = "[%a_][%w_]*", type = "symbol" }, }, symbols = { ["if"] = "keyword", @@ -48,6 +165,7 @@ syntax.add { ["go"] = "keyword", ["fallthrough"] = "keyword", ["goto"] = "keyword", + ["iota"] = "keyword2", ["int"] = "keyword2", ["int64"] = "keyword2", ["int32"] = "keyword2", diff --git a/plugins/language_hlsl.lua b/plugins/language_hlsl.lua index 696e9b2..e1d5570 100644 --- a/plugins/language_hlsl.lua +++ b/plugins/language_hlsl.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local style = require "core.style" local common = require "core.common" diff --git a/plugins/language_hs.lua b/plugins/language_hs.lua index a60ff75..4271210 100644 --- a/plugins/language_hs.lua +++ b/plugins/language_hs.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_htaccess.lua b/plugins/language_htaccess.lua index 2cb71ca..acfa419 100644 --- a/plugins/language_htaccess.lua +++ b/plugins/language_htaccess.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" local keywords = { diff --git a/plugins/language_ini.lua b/plugins/language_ini.lua index 706cc80..2f5d3b5 100644 --- a/plugins/language_ini.lua +++ b/plugins/language_ini.lua @@ -1,4 +1,4 @@ --- mod-version:2 +-- mod-version:3 local syntax = require "core.syntax" diff --git a/plugins/language_java.lua b/plugins/language_java.lua index 810ffd2..30c1249 100644 --- a/plugins/language_java.lua +++ b/plugins/language_java.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_jiyu.lua b/plugins/language_jiyu.lua index 78cc377..dbf9d0e 100644 --- a/plugins/language_jiyu.lua +++ b/plugins/language_jiyu.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_jsx.lua b/plugins/language_jsx.lua index 1cbc4ac..04e846b 100644 --- a/plugins/language_jsx.lua +++ b/plugins/language_jsx.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 -- Almost identical to JS, with the exception that / shouldn't denote a regex. Current JS syntax highlighter will highlight half the document due to closing tags. local syntax = require "core.syntax" diff --git a/plugins/language_julia.lua b/plugins/language_julia.lua index e62a9b2..347592f 100644 --- a/plugins/language_julia.lua +++ b/plugins/language_julia.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 -- Support for the Julia programming language: -- Covers the most used keywords up to Julia version 1.6.4 diff --git a/plugins/language_liquid.lua b/plugins/language_liquid.lua index 58688c8..ad40a79 100644 --- a/plugins/language_liquid.lua +++ b/plugins/language_liquid.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" local liquid_syntax = { @@ -92,7 +92,7 @@ local liquid_syntax = { ["upcase"] = "keyword2", ["when"] = "keyword", ["where"] = "keyword2" - + }, } @@ -102,27 +102,27 @@ syntax.add { patterns = { { pattern = { "{%%", "%%}" }, syntax = liquid_syntax, type = "function" }, { pattern = { "{{", "}}" }, syntax = liquid_syntax, type = "function" }, - { - pattern = { + { + pattern = { "<%s*[sS][cC][rR][iI][pP][tT]%s+[tT][yY][pP][eE]%s*=%s*" .. "['\"]%a+/[jJ][aA][vV][aA][sS][cC][rR][iI][pP][tT]['\"]%s*>", - "<%s*/[sS][cC][rR][iI][pP][tT]>" + "<%s*/[sS][cC][rR][iI][pP][tT]>" }, - syntax = ".js", - type = "function" + syntax = ".js", + type = "function" }, - { - pattern = { + { + pattern = { "<%s*[sS][cC][rR][iI][pP][tT]%s*>", - "<%s*/%s*[sS][cC][rR][iI][pP][tT]>" + "<%s*/%s*[sS][cC][rR][iI][pP][tT]>" }, syntax = ".js", type = "function" }, - { - pattern = { - "<%s*[sS][tT][yY][lL][eE][^>]*>", - "<%s*/%s*[sS][tT][yY][lL][eE]%s*>" + { + pattern = { + "<%s*[sS][tT][yY][lL][eE][^>]*>", + "<%s*/%s*[sS][tT][yY][lL][eE]%s*>" }, syntax = ".css", type = "function" diff --git a/plugins/language_lobster.lua b/plugins/language_lobster.lua index b6ab143..24f4251 100644 --- a/plugins/language_lobster.lua +++ b/plugins/language_lobster.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { @@ -46,7 +46,7 @@ syntax.add { ["string"] = "keyword2", ["any"] = "keyword2", ["void"] = "keyword2", - + ["is"] = "keyword", ["typeof"] = "keyword", ["var"] = "keyword", @@ -59,7 +59,7 @@ syntax.add { ["not"] = "operator", ["and"] = "operator", - ["or"] = "operator", + ["or"] = "operator", ["nil"] = "literal", }, diff --git a/plugins/language_make.lua b/plugins/language_make.lua index 687b827..4ad3521 100644 --- a/plugins/language_make.lua +++ b/plugins/language_make.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_meson.lua b/plugins/language_meson.lua index f5c55d7..be10d66 100644 --- a/plugins/language_meson.lua +++ b/plugins/language_meson.lua @@ -1,9 +1,9 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { name = "Meson", - files = "meson.build$", + files = { "^meson%.build$", "^meson_options%.txt$" }, comment = "#", patterns = { { pattern = { "#", "\n" }, type = "comment" }, diff --git a/plugins/language_miniscript.lua b/plugins/language_miniscript.lua new file mode 100644 index 0000000..afe0a2b --- /dev/null +++ b/plugins/language_miniscript.lua @@ -0,0 +1,102 @@ +-- mod-version:3 +local syntax = require 'core.syntax' + +syntax.add { + name = "MiniScript", + files = { "%.ms$" }, + comment = "//", + patterns = { + { pattern = "//.*", type = "comment" }, + { pattern = { '"', '"' }, type = "string" }, + { pattern = "[<>!=]=", type = "operator" }, + { pattern = "[%+%-%*%/%^@<>:]", type = "operator" }, + { pattern = "%d%.%d*[eE][-+]?%d+", type = "number" }, + { pattern = "%d%.%d*", type = "number" }, + { pattern = "%.?%d*[eE][-+]?%d+", type = "number" }, + { pattern = "%.?%d+", type = "number" }, + { pattern = "[%a_][%w_]*", type = "symbol" }, + }, + symbols = { + ["if"] = "keyword", + ["not"] = "keyword", + ["and"] = "keyword", + ["or"] = "keyword", + ["else"] = "keyword", + ["then"] = "keyword", + ["for"] = "keyword", + ["in"] = "keyword", + ["while"] = "keyword", + ["break"] = "keyword", + ["continue"] = "keyword", + ["function"] = "keyword", + ["end"] = "keyword", + ["return"] = "keyword", + ["new"] = "keyword", + ["isa"] = "keyword", + ["self"] = "keyword2", + + ["true"] = "literal", + ["false"] = "literal", + ["null"] = "literal", + ["globals"] = "literal", + ["locals"] = "literal", + ["outer"] = "literal", + + -- Built-in types's classes + ["number"] = "literal", + ["string"] = "literal", + ["list"] = "literal", + ["map"] = "literal", + ["funcRef"] = "literal", + + -- Intrinsic functions + ["abs"] = "function", + ["acos"] = "function", + ["asin"] = "function", + ["atan"] = "function", + ["bitAnd"] = "function", + ["bitOr"] = "function", + ["bitXor"] = "function", + ["ceil"] = "function", + ["char"] = "function", + ["code"] = "function", + ["cos"] = "function", + ["floor"] = "function", + ["hash"] = "function", + ["hasIndex"] = "function", + ["indexes"] = "function", + ["indexOf"] = "function", + ["insert"] = "function", + ["join"] = "function", + ["len"] = "function", + ["log"] = "function", + ["lower"] = "function", + ["pi"] = "function", + ["pop"] = "function", + ["print"] = "function", + ["pull"] = "function", + ["push"] = "function", + ["range"] = "function", + ["remove"] = "function", + ["replace"] = "function", + ["rnd"] = "function", + ["round"] = "function", + ["shuffle"] = "function", + ["sign"] = "function", + ["sin"] = "function", + ["slice"] = "function", + ["sort"] = "function", + ["split"] = "function", + ["sqrt"] = "function", + ["str"] = "function", + ["sum"] = "function", + ["tan"] = "function", + ["time"] = "function", + ["upper"] = "function", + ["val"] = "function", + ["values"] = "function", + ["version"] = "function", + ["wait"] = "function", + ["yield"] = "function", + }, +} diff --git a/plugins/language_moon.lua b/plugins/language_moon.lua index 59eea37..8042b7d 100644 --- a/plugins/language_moon.lua +++ b/plugins/language_moon.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_nginx.lua b/plugins/language_nginx.lua index 73cf979..7124c1a 100644 --- a/plugins/language_nginx.lua +++ b/plugins/language_nginx.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" -- Copied from https://github.com/shanoor/vscode-nginx/blob/master/syntaxes/nginx.tmLanguage diff --git a/plugins/language_nim.lua b/plugins/language_nim.lua index 5f00365..d7726d1 100644 --- a/plugins/language_nim.lua +++ b/plugins/language_nim.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" local patterns = {} diff --git a/plugins/language_objc.lua b/plugins/language_objc.lua index e0945d2..8d1210b 100644 --- a/plugins/language_objc.lua +++ b/plugins/language_objc.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_odin.lua b/plugins/language_odin.lua index ff75700..a5dfa45 100644 --- a/plugins/language_odin.lua +++ b/plugins/language_odin.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_perl.lua b/plugins/language_perl.lua index 1938b1b..0e057e2 100644 --- a/plugins/language_perl.lua +++ b/plugins/language_perl.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_php.lua b/plugins/language_php.lua index 5ef4f22..2820862 100644 --- a/plugins/language_php.lua +++ b/plugins/language_php.lua @@ -1,23 +1,152 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 --[[ language_php.lua provides php syntax support allowing mixed html, css and js - version: 20210902_1 + version: 20220614_1 --]] local syntax = require "core.syntax" +local common = require "core.common" +local config = require "core.config" -- load syntax dependencies to add additional rules require "plugins.language_css" require "plugins.language_js" --- generate SQL string marker regex -local sql_markers = { 'create', 'select', 'insert', 'update', 'replace', 'delete', 'drop', 'alter' } -local sql_regex = {} -for _,marker in ipairs(sql_markers) do - table.insert(sql_regex, marker) - table.insert(sql_regex, string.upper(marker)) +local psql_found = pcall(require, "plugins.language_psql") +local sql_strings = {} + +config.plugins.language_php = common.merge({ + sql_strings = true, + -- The config specification used by the settings gui + config_spec = { + name = "Language PHP", + { + label = "SQL Strings", + description = "Highlight as SQL, strings starting with sql statements, " + .. "depends on language_psql.", + path = "sql_strings", + type = "toggle", + default = true, + on_apply = function(enabled) + local syntax_php = syntax.get("file.phps") + if enabled and psql_found then + if + not syntax_php.patterns[6].syntax + or + syntax_php.patterns[6].syntax ~= '.sql' + then + table.insert(syntax_php.patterns, 5, sql_strings[1]) + table.insert(syntax_php.patterns, 6, sql_strings[2]) + end + elseif + syntax_php.patterns[6].syntax + and + syntax_php.patterns[6].syntax == '.sql' + then + table.remove(syntax_php.patterns, 5) + table.remove(syntax_php.patterns, 5) + end + end + } + } +}, config.plugins.language_php) + +-- Patterns to match some of the string inline variables +local inline_variables = { + { pattern = "%s+", type = "string" }, + { pattern = "\\%$", type = "string" }, + { pattern = "%{[%$%s]*%}", type = "string" }, + -- matches {$varname[index]} + { pattern = "{" + .. "()%$[%a_][%w_]*" + .. "()%[" + .. "()[%w%s_%-\"\'%(%)|;:,%.#@%%!%^&%*%+=%[%]<>~`%?\\/]*" + .. "()%]" + .. "}", + type = { + "keyword", "keyword2", "keyword", "string", "keyword" + } + }, + { pattern = "{" + .. "()%$[%a_][%w_]*" + .. "()%->" + .. "()[%a_][%w_]*" + .. "()}", + type = { + "keyword", "keyword2", "keyword", "symbol", "keyword" + } + }, + { pattern = "{()%$[%a_][%w_]*()}", + type = { "keyword", "keyword2", "keyword" } + }, + { pattern = "%$[%a_][%w_]*()%[()%w*()%]", + type = { "keyword2", "keyword", "string", "keyword" } + }, + { pattern = "%$[%a_][%w_]*()%->()%w+", + type = { "keyword2", "keyword", "symbol" } + }, + { pattern = "%$[%a_][%w_]*", type = "keyword2" }, + { pattern = "%w+", type = "string" }, + { pattern = "[^\"]", type = "string" }, +} + +local function combine_patterns(t1, t2) + local temp = { table.unpack(t1) } + for _, t in ipairs(t2) do + table.insert(temp, t) + end + return temp +end + +local function clone(tbl) + local t = {} + if tbl then + for k, v in pairs(tbl) do + if type(v) == "table" then + t[k] = clone(v) + else + t[k] = v + end + end + end + return t +end + +-- optionally allow sql syntax on strings +if psql_found then + -- generate SQL string marker regex + local sql_markers = { 'create', 'select', 'insert', 'update', 'replace', 'delete', 'drop', 'alter' } + local sql_regex = {} + for _, marker in ipairs(sql_markers) do + table.insert(sql_regex, marker) + table.insert(sql_regex, string.upper(marker)) + end + sql_regex = table.concat(sql_regex, '|') + + -- inject inline variable rules to cloned psql syntax + local syntax_phpsql = clone(syntax.get("file.sql")) + syntax_phpsql.name = "PHP SQL" + syntax_phpsql.files = "%.phpsql$" + table.insert(syntax_phpsql.patterns, 2, { pattern = "\\%$", type = "symbol" }) + table.insert(syntax_phpsql.patterns, 3, { pattern = "%{[%$%s]*%}", type = "symbol" }) + for i=4, 9 do + table.insert(syntax_phpsql.patterns, i, inline_variables[i]) + end + + -- SQL strings + sql_strings = { + { + regex = { '"(?=(?:'..sql_regex..')\\s+)', '"', '\\' }, + syntax = syntax_phpsql, + type = "string" + }, + { + regex = { '\'(?=(?:'..sql_regex..')\\s+)', '\'', '\\' }, + syntax = '.sql', + type = "string" + }, + } end -sql_regex = table.concat(sql_regex, '|') -- define the core php syntax coloring syntax.add { @@ -33,20 +162,36 @@ syntax.add { { pattern = "//.-\n", type = "comment" }, { pattern = "#.-\n", type = "comment" }, { pattern = { "/%*", "%*/" }, type = "comment" }, - -- SQL strings - { - regex = { '"(?=(?:'..sql_regex..')\\s+)', '"', '\\' }, - syntax = '.sql', - type = "string" + -- Single quote string + { pattern = { "'", "'", '\\' }, type = "string" }, + { pattern = { "<<<'%a%w*'\n", "^%s*%a%w*%f[;]", '\\' }, + type = "string" }, - { - regex = { '\'(?=(?:'..sql_regex..')\\s+)', '\'', '\\' }, - syntax = '.sql', - type = "string" + -- Strings with support for some inline variables syntax + { pattern = { "<<<%a%w*\n", "^%s*%a%w*%f[;]", '\\' }, + syntax = { + patterns = combine_patterns(inline_variables, { + -- prevent matching outside of the parent string + { pattern = "^%s*%a%w*();$", + type = { "string", "normal" } + }, + { pattern = "%p", type = "string" }, + }), + symbols = {} + }, + type = "string" + }, + { pattern = { '"', '"', '\\' }, + syntax = { + patterns = combine_patterns(inline_variables, { + -- prevent matching outside of the parent string + { pattern = "%p+%f[\"]", type = "string" }, + { pattern = "%p", type = "string" }, + }), + symbols = {} + }, + type = "string" }, - -- The '\\' is for escaping to work on " or ' - { pattern = { '"', '"', '\\' }, type = "string" }, - { pattern = { "'", "'", '\\' }, type = "string" }, { pattern = "0[bB][%d]+", type = "number" }, { pattern = "0[xX][%da-fA-F]+", type = "number" }, { pattern = "-?%d[%d_%.eE]*", type = "number" }, @@ -171,6 +316,13 @@ syntax.add { }, } +-- insert sql string rules after the "/%*", "%*/" pattern +if psql_found and config.plugins.language_php.sql_strings then + local syntax_php = syntax.get("file.phps") + table.insert(syntax_php.patterns, 5, sql_strings[1]) + table.insert(syntax_php.patterns, 6, sql_strings[2]) +end + -- allows html, css and js coloring on php files syntax.add { name = "PHP", diff --git a/plugins/language_pico8.lua b/plugins/language_pico8.lua index 40c9772..aefd6f6 100644 --- a/plugins/language_pico8.lua +++ b/plugins/language_pico8.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_pkgbuild.lua b/plugins/language_pkgbuild.lua index de38d5c..049a5d1 100644 --- a/plugins/language_pkgbuild.lua +++ b/plugins/language_pkgbuild.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_po.lua b/plugins/language_po.lua index db060b1..a1b098c 100644 --- a/plugins/language_po.lua +++ b/plugins/language_po.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_powershell.lua b/plugins/language_powershell.lua index 63de2f3..fdba844 100644 --- a/plugins/language_powershell.lua +++ b/plugins/language_powershell.lua @@ -1,74 +1,77 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { - name = "PowerShell", - files = {"%.ps1$", "%.psm1$", "%.psd1$", "%.ps1xml$", "%.pssc$", "%.psrc$", "%.cdxml$"}, - comment = "#", - patterns = { - {pattern = "#.*\n", type = "comment"}, - {pattern = [[\.]], type = "normal"}, - {pattern = {'"', '"'}, type = "string"}, - {pattern = {"'", "'"}, type = "string"}, - {pattern = "%f[%w_][%d%.]+%f[^%w_]", type = "number"}, - {pattern = "[%+=/%*%^%%<>!~|&,:]+", type = "operator"}, - {pattern = "%f[%S]%-[%w%-_]+", type = "function"}, - {pattern = "[%u][%a]+[%-][%u][%a]+", type = "function"}, - {pattern = "${.*}", type = "symbol"}, - {pattern = "$[%a_@*][%w_]*", type = "keyword2"}, - {pattern = "$[%$][%a]+", type = "keyword2"}, - {pattern = "[%a_][%w_]*", type = "symbol"} - }, - symbols = { - ["if"] = "keyword", - ["else"] = "keyword", - ["elseif"] = "keyword", - ["switch"] = "keyword", - ["default"] = "keyword", - ["function"] = "keyword", - ["filter"] = "keyword", - ["workflow"] = "keyword", - ["configuration"] = "keyword", - ["class"] = "keyword", - ["enum"] = "keyword", - ["Parameter"] = "keyword", - ["ValidateScript"] = "keyword", - ["CmdletBinding"] = "keyword", - ["try"] = "keyword", - ["catch"] = "keyword", - ["finally"] = "keyword", - ["throw"] = "keyword", - ["while"] = "keyword", - ["for"] = "keyword", - ["do"] = "keyword", - ["until"] = "keyword", - ["break"] = "keyword", - ["continue"] = "keyword", - ["foreach"] = "keyword", - ["in"] = "keyword", - ["return"] = "keyword", - ["where"] = "function", - ["select"] = "function", - ["filter"] = "keyword", - ["in"] = "keyword", - ["trap"] = "keyword", - ["param"] = "keyword", - ["data"] = "keyword", - ["dynamicparam"] = "keyword", - ["begin"] = "function", - ["process"] = "function", - ["end"] = "function", - ["exit"] = "function", - ["inlinescript"] = "function", - ["parallel"] = "function", - ["sequence"] = "function", - ["true"] = "literal", - ["false"] = "literal", - ["TODO"] = "comment", - ["FIXME"] = "comment", - ["XXX"] = "comment", - ["TBD"] = "comment", - ["HACK"] = "comment", - ["NOTE"] = "comment" - } + name = "PowerShell", + files = { + "%.ps1$", "%.psm1$", "%.psd1$", "%.ps1xml$", + "%.pssc$", "%.psrc$", "%.cdxml$" + }, + comment = "#", + patterns = { + {pattern = "#.*\n", type = "comment"}, + {pattern = [[\.]], type = "normal"}, + {pattern = {'"', '"'}, type = "string"}, + {pattern = {"'", "'"}, type = "string"}, + {pattern = "%f[%w_][%d%.]+%f[^%w_]", type = "number"}, + {pattern = "[%+=/%*%^%%<>!~|&,:]+", type = "operator"}, + {pattern = "%f[%S]%-[%w%-_]+", type = "function"}, + {pattern = "[%u][%a]+[%-][%u][%a]+", type = "function"}, + {pattern = "${.*}", type = "symbol"}, + {pattern = "$[%a_@*][%w_]*", type = "keyword2"}, + {pattern = "$[%$][%a]+", type = "keyword2"}, + {pattern = "[%a_][%w_]*", type = "symbol"} + }, + symbols = { + ["if"] = "keyword", + ["else"] = "keyword", + ["elseif"] = "keyword", + ["switch"] = "keyword", + ["default"] = "keyword", + ["function"] = "keyword", + ["filter"] = "keyword", + ["workflow"] = "keyword", + ["configuration"] = "keyword", + ["class"] = "keyword", + ["enum"] = "keyword", + ["Parameter"] = "keyword", + ["ValidateScript"] = "keyword", + ["CmdletBinding"] = "keyword", + ["try"] = "keyword", + ["catch"] = "keyword", + ["finally"] = "keyword", + ["throw"] = "keyword", + ["while"] = "keyword", + ["for"] = "keyword", + ["do"] = "keyword", + ["until"] = "keyword", + ["break"] = "keyword", + ["continue"] = "keyword", + ["foreach"] = "keyword", + ["in"] = "keyword", + ["return"] = "keyword", + ["where"] = "function", + ["select"] = "function", + ["filter"] = "keyword", + ["in"] = "keyword", + ["trap"] = "keyword", + ["param"] = "keyword", + ["data"] = "keyword", + ["dynamicparam"] = "keyword", + ["begin"] = "function", + ["process"] = "function", + ["end"] = "function", + ["exit"] = "function", + ["inlinescript"] = "function", + ["parallel"] = "function", + ["sequence"] = "function", + ["true"] = "literal", + ["false"] = "literal", + ["TODO"] = "comment", + ["FIXME"] = "comment", + ["XXX"] = "comment", + ["TBD"] = "comment", + ["HACK"] = "comment", + ["NOTE"] = "comment" + } } diff --git a/plugins/language_psql.lua b/plugins/language_psql.lua index 80bc4cd..b891dae 100644 --- a/plugins/language_psql.lua +++ b/plugins/language_psql.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" -- In sql symbols can be lower case and upper case diff --git a/plugins/language_rescript.lua b/plugins/language_rescript.lua index 9007dc1..3d3bca0 100644 --- a/plugins/language_rescript.lua +++ b/plugins/language_rescript.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_rivet.lua b/plugins/language_rivet.lua index 61b11bc..214d76b 100644 --- a/plugins/language_rivet.lua +++ b/plugins/language_rivet.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 -- Syntax highlighting for the Rivet programming language. -- by StunxFS :) @@ -24,7 +24,7 @@ syntax.add { {pattern = "-?%.?%d+", type = "number"}, {pattern = "[%+%-=/%*%^%%<>!~|&%.%?]", type = "operator"}, -- Uppercase constants of at least 2 chars in length - { + { pattern = "_?%u[%u_][%u%d_]*%f[%s%+%*%-%.%)%]}%?%^%%=/<>~|&;:,!]", type = "number" }, @@ -80,7 +80,7 @@ syntax.add { ["is"] = "keyword", ["in"] = "keyword", ["as"] = "keyword", - + -- types ["no_return"] = "keyword2", ["bool"] = "keyword2", diff --git a/plugins/language_ruby.lua b/plugins/language_ruby.lua index 85717fe..c46d558 100644 --- a/plugins/language_ruby.lua +++ b/plugins/language_ruby.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { @@ -23,7 +23,6 @@ syntax.add { }, symbols = { ["nil"] = "literal", - ["end"] = "literal", ["true"] = "literal", ["false"] = "literal", ["private"] = "keyword", @@ -63,7 +62,6 @@ syntax.add { ["self"] = "keyword", ["super"] = "keyword", ["then"] = "keyword", - ["true"] = "keyword", ["undef"] = "keyword", ["unless"] = "keyword", ["until"] = "keyword", diff --git a/plugins/language_rust.lua b/plugins/language_rust.lua index f20d35e..848e8b1 100644 --- a/plugins/language_rust.lua +++ b/plugins/language_rust.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { @@ -23,68 +23,66 @@ syntax.add { { pattern = "[%a_][%w_]*", type = "symbol" }, }, symbols = { - ["as"] = "keyword", - ["async"] = "keyword", - ["await"] = "keyword", - ["break"] = "keyword", - ["const"] = "keyword", - ["continue"] = "keyword", - ["crate"] = "keyword", - ["dyn"] = "keyword", - ["else"] = "keyword", - ["enum"] = "keyword", - ["extern"] = "keyword", - ["false"] = "keyword", - ["fn"] = "keyword", - ["for"] = "keyword", - ["if"] = "keyword", - ["impl"] = "keyword", - ["in"] = "keyword", - ["let"] = "keyword", - ["loop"] = "keyword", - ["match"] = "keyword", - ["mod"] = "keyword", - ["move"] = "keyword", - ["mut"] = "keyword", - ["pub"] = "keyword", - ["ref"] = "keyword", - ["return"] = "keyword", - ["Self"] = "keyword", - ["self"] = "keyword", - ["static"] = "keyword", - ["struct"] = "keyword", - ["super"] = "keyword", - ["trait"] = "keyword", - ["true"] = "keyword", - ["type"] = "keyword", - ["unsafe"] = "keyword", - ["use"] = "keyword", - ["where"] = "keyword", - ["while"] = "keyword", - ["i32"] = "keyword2", - ["i64"] = "keyword2", - ["i128"] = "keyword2", - ["i16"] = "keyword2", - ["i8"] = "keyword2", - ["u8"] = "keyword2", - ["u16"] = "keyword2", - ["u32"] = "keyword2", - ["u64"] = "keyword2", - ["usize"] = "keyword2", - ["isize"] = "keyword2", - ["f32"] = "keyword2", - ["f64"] = "keyword2", - ["f128"] = "keyword2", - ["String"] = "keyword2", - ["char"] = "keyword2", - ["str"] = "keyword2", - ["bool"] = "keyword2", - ["true"] = "literal", - ["false"] = "literal", - ["None"] = "literal", - ["Some"] = "literal", - ["Option"] = "literal", - ["Result"] = "literal", + ["as"] = "keyword", + ["async"] = "keyword", + ["await"] = "keyword", + ["break"] = "keyword", + ["const"] = "keyword", + ["continue"] = "keyword", + ["crate"] = "keyword", + ["dyn"] = "keyword", + ["else"] = "keyword", + ["enum"] = "keyword", + ["extern"] = "keyword", + ["fn"] = "keyword", + ["for"] = "keyword", + ["if"] = "keyword", + ["impl"] = "keyword", + ["in"] = "keyword", + ["let"] = "keyword", + ["loop"] = "keyword", + ["match"] = "keyword", + ["mod"] = "keyword", + ["move"] = "keyword", + ["mut"] = "keyword", + ["pub"] = "keyword", + ["ref"] = "keyword", + ["return"] = "keyword", + ["Self"] = "keyword", + ["self"] = "keyword", + ["static"] = "keyword", + ["struct"] = "keyword", + ["super"] = "keyword", + ["trait"] = "keyword", + ["type"] = "keyword", + ["unsafe"] = "keyword", + ["use"] = "keyword", + ["where"] = "keyword", + ["while"] = "keyword", + ["i32"] = "keyword2", + ["i64"] = "keyword2", + ["i128"] = "keyword2", + ["i16"] = "keyword2", + ["i8"] = "keyword2", + ["u8"] = "keyword2", + ["u16"] = "keyword2", + ["u32"] = "keyword2", + ["u64"] = "keyword2", + ["usize"] = "keyword2", + ["isize"] = "keyword2", + ["f32"] = "keyword2", + ["f64"] = "keyword2", + ["f128"] = "keyword2", + ["String"] = "keyword2", + ["char"] = "keyword2", + ["str"] = "keyword2", + ["bool"] = "keyword2", + ["true"] = "literal", + ["false"] = "literal", + ["None"] = "literal", + ["Some"] = "literal", + ["Option"] = "literal", + ["Result"] = "literal", }, } diff --git a/plugins/language_sass.lua b/plugins/language_sass.lua index 723d975..f440927 100644 --- a/plugins/language_sass.lua +++ b/plugins/language_sass.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_scala.lua b/plugins/language_scala.lua index fddadc7..6c5d7a7 100644 --- a/plugins/language_scala.lua +++ b/plugins/language_scala.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { @@ -19,62 +19,62 @@ syntax.add { { pattern = "[%a_][%w_]*", type = "symbol" }, }, symbols = { - ["abstract"] = "keyword", - ["case"] = "keyword", - ["catch"] = "keyword", - ["class"] = "keyword", - ["finally"] = "keyword", - ["final"] = "keyword", - ["do"] = "keyword", - ["extends"] = "keyword", - ["forSome"] = "keyword", - ["implicit"] = "keyword", - ["lazy"] = "keyword", - ["match"] = "keyword", - ["new"] = "keyword", - ["override"] = "keyword", - ["package"] = "keyword", - ["throw"] = "keyword", - ["trait"] = "keyword", - ["type"] = "keyword", - ["var"] = "keyword", - ["val"] = "keyword", + ["abstract"] = "keyword", + ["case"] = "keyword", + ["catch"] = "keyword", + ["class"] = "keyword", + ["finally"] = "keyword", + ["final"] = "keyword", + ["do"] = "keyword", + ["extends"] = "keyword", + ["forSome"] = "keyword", + ["implicit"] = "keyword", + ["lazy"] = "keyword", + ["match"] = "keyword", + ["new"] = "keyword", + ["override"] = "keyword", + ["package"] = "keyword", + ["throw"] = "keyword", + ["trait"] = "keyword", + ["type"] = "keyword", + ["var"] = "keyword", + ["val"] = "keyword", ["println"] = "keyword", - ["return"] = "keyword", - ["for"] = "keyword", - ["Try"] = "keyword", - ["def"] = "keyword", - ["while"] = "keyword", - ["with"] = "keyword", - ["if"] = "keyword", - ["else"] = "keyword", - ["import"] = "keyword", - ["object"] = "keyword", - ["yield"] = "keyword", - - ["private"] = "keyword2", - ["protected"] = "keyword2", - ["sealed"] = "keyword2", - ["super"] = "keyword2", - ["this"] = "keyword2", - ["Byte"] = "keyword2", - ["Short"] = "keyword2", - ["Int"] = "keyword2", - ["Long"] = "keyword2", - ["Float"] = "keyword2", - ["Double"] = "keyword2", - ["Char"] = "keyword2", - ["String"] = "keyword2", - ["List"] = "keyword2", - ["Array"] = "keyword2", - ["Boolean"] = "keyword2", - - ["Null"] = "literal", - ["Any"] = "literal", - ["AnyRef"] = "literal", - ["Nothing"] = "literal", - ["Unit"] = "literal", - ["true"] = "literal", - ["false"] = "literal", + ["return"] = "keyword", + ["for"] = "keyword", + ["Try"] = "keyword", + ["def"] = "keyword", + ["while"] = "keyword", + ["with"] = "keyword", + ["if"] = "keyword", + ["else"] = "keyword", + ["import"] = "keyword", + ["object"] = "keyword", + ["yield"] = "keyword", + + ["private"] = "keyword2", + ["protected"] = "keyword2", + ["sealed"] = "keyword2", + ["super"] = "keyword2", + ["this"] = "keyword2", + ["Byte"] = "keyword2", + ["Short"] = "keyword2", + ["Int"] = "keyword2", + ["Long"] = "keyword2", + ["Float"] = "keyword2", + ["Double"] = "keyword2", + ["Char"] = "keyword2", + ["String"] = "keyword2", + ["List"] = "keyword2", + ["Array"] = "keyword2", + ["Boolean"] = "keyword2", + + ["Null"] = "literal", + ["Any"] = "literal", + ["AnyRef"] = "literal", + ["Nothing"] = "literal", + ["Unit"] = "literal", + ["true"] = "literal", + ["false"] = "literal", } } diff --git a/plugins/language_sh.lua b/plugins/language_sh.lua index fdfe867..5d7b987 100644 --- a/plugins/language_sh.lua +++ b/plugins/language_sh.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_ssh_config.lua b/plugins/language_ssh_config.lua index c105082..894ce21 100644 --- a/plugins/language_ssh_config.lua +++ b/plugins/language_ssh_config.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_tcl.lua b/plugins/language_tcl.lua index 1fb672e..7a6e13f 100644 --- a/plugins/language_tcl.lua +++ b/plugins/language_tcl.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_teal.lua b/plugins/language_teal.lua index a974fdd..0b0e35d 100644 --- a/plugins/language_teal.lua +++ b/plugins/language_teal.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_tex.lua b/plugins/language_tex.lua index 264c5ca..f8e69e2 100644 --- a/plugins/language_tex.lua +++ b/plugins/language_tex.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_toml.lua b/plugins/language_toml.lua index 2b7fd01..02e5b58 100644 --- a/plugins/language_toml.lua +++ b/plugins/language_toml.lua @@ -1,4 +1,4 @@ --- mod-version:2 +-- mod-version:3 local syntax = require "core.syntax" diff --git a/plugins/language_ts.lua b/plugins/language_ts.lua index 13c6ac2..f28261d 100644 --- a/plugins/language_ts.lua +++ b/plugins/language_ts.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 -- copied from language_js, but added regex highlighting back local syntax = require "core.syntax" diff --git a/plugins/language_tsx.lua b/plugins/language_tsx.lua index 473f808..beedbc6 100644 --- a/plugins/language_tsx.lua +++ b/plugins/language_tsx.lua @@ -1,5 +1,6 @@ --- mod-version:2 -- lite-xl 2.0 --- Almost identical to JS, with the exception that / shouldn't denote a regex. Current JS syntax highlighter will highlight half the document due to closing tags. +-- mod-version:3 +-- Almost identical to JS, with the exception that / shouldn't denote a regex. +-- Current JS syntax highlighter will highlight half the document due to closing tags. local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_v.lua b/plugins/language_v.lua index 4afd3fd..d5b9764 100644 --- a/plugins/language_v.lua +++ b/plugins/language_v.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_wren.lua b/plugins/language_wren.lua index 9bd6c82..2022dbf 100644 --- a/plugins/language_wren.lua +++ b/plugins/language_wren.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { @@ -22,19 +22,15 @@ syntax.add { ["class"] = "keyword", ["construct"] = "keyword", ["else"] = "keyword", - ["false"] = "keyword", ["for"] = "keyword", ["foreign"] = "keyword", ["if"] = "keyword", ["import"] = "keyword", ["in"] = "keyword", ["is"] = "keyword", - ["null"] = "keyword", ["return"] = "keyword", ["static"] = "keyword", ["super"] = "keyword", - ["this"] = "keyword", - ["true"] = "keyword", ["var"] = "keyword", ["while"] = "keyword", ["this"] = "keyword2", diff --git a/plugins/language_yaml.lua b/plugins/language_yaml.lua index a83e89f..8b7f634 100644 --- a/plugins/language_yaml.lua +++ b/plugins/language_yaml.lua @@ -1,53 +1,149 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" +local yaml_bracket_list = { + patterns = { + -- comments + { pattern = { "#", "\n"}, type = "comment" }, + -- strings + { pattern = { '"', '"', '\\' }, type = "string" }, + { pattern = { "'", "'", '\\' }, type = "string" }, + -- keys + { + pattern = "[%w%d]+%g+()%s*():()%s", + type = { "keyword2", "normal", "operator", "normal" } + }, + -- variables + { pattern = "%$%a%w+", type = "keyword" }, + { pattern = "%$%{%{.-%}%}", type = "keyword" }, + -- numeric place holders + { pattern = "%-?%.inf", type = "number" }, + { pattern = "%.NaN", type = "number" }, + -- numbers + { pattern = "[%+%-]?0%d+", type = "number" }, + { pattern = "[%+%-]?0x%x+", type = "number" }, + { pattern = "[%+%-]?%d+[,%.eE:%+%d]*%d+", type = "number" }, + { pattern = "[%+%-]?%d+", type = "number" }, + -- others + { pattern = ",", type = "operator" }, + { pattern = "%w+", type = "string" }, + { + pattern = "[_%(%)%*@~`!%%%^&=%+%-\\;%.><%?/%s]+", + type = "string" + } + }, + symbols = {} +} + syntax.add { name = "YAML", files = { "%.yml$", "%.yaml$" }, comment = "#", + space_handling = false, patterns = { + --- rules that start with spaces first and those taking precedence + -- parent and child keys + { + pattern = "^[%w%d]+%g+%s*%f[:]", + type = "literal" + }, + { + pattern = "^%s+[%w%d]+%g+%s*%f[:]", + type = "keyword2" + }, + -- bracket lists after key declaration + { + pattern = { ":%s+%[", "%]" }, + syntax = yaml_bracket_list, type = "operator" + }, + { + pattern = { ":%s+{", "}" }, + syntax = yaml_bracket_list, type = "operator" + }, + -- child key + { + pattern = "^%s+()[%w%d]+%g+()%s*():()%s", + type = { "normal", "keyword2", "normal", "operator", "normal" } + }, + -- child list element + { + pattern = "^%s+()%-()%s+()[%w%d]+%g+()%s*():()%s", + type = { "normal", "operator", "normal", "keyword2", "normal", "operator", "normal" } + }, + -- unkeyed bracket lists + { + pattern = { "^%s*%[", "%]" }, + syntax = yaml_bracket_list, type = "operator" + }, + { + pattern = { "^%s*{", "}" }, + syntax = yaml_bracket_list, type = "operator" + }, + { + pattern = { "^%s*%-%s*%[", "%]" }, + syntax = yaml_bracket_list, type = "operator" + }, + { + pattern = { "^%s*%-%s*{", "}" }, + syntax = yaml_bracket_list, type = "operator" + }, + -- rule to optimize space handling + { pattern = "%s+", type = "normal" }, + --- all the other rules + -- comments { pattern = { "#", "\n"}, type = "comment" }, + -- strings { pattern = { '"', '"', '\\' }, type = "string" }, { pattern = { "'", "'", '\\' }, type = "string" }, + -- extra bracket lists rules on explicit type + { + pattern = { "!!%w+%s+%[", "%]"}, + syntax = yaml_bracket_list, type = "operator" + }, + { + pattern = { "!!%w+%s+{", "}"}, + syntax = yaml_bracket_list, type = "operator" + }, + -- numeric place holders { pattern = "%-?%.inf", type = "number" }, { pattern = "%.NaN", type = "number" }, + -- parent list element + { + pattern = "^%-()%s+()[%w%d]+%g+()%s*():()%s", + type = { "operator", "normal", "keyword2", "normal", "operator", "normal" } + }, + -- key label { pattern = "%&()%g+", type = { "keyword", "literal" } }, - { pattern = "!%g+", type = "keyword" }, + -- key elements expansion { pattern = "<<", type = "literal" }, { - pattern = "[%s]%*()[%w%d_]+", - type = { "keyword", "keyword2" } - }, - { pattern = "%*()[%w%d_]+", type = { "keyword", "literal" } }, + -- explicit data types + { pattern = "!!%g+", type = "keyword" }, + -- parent key { - pattern = "[%[%{]()%s*[%w%d]+%g+%s*():%s", - type = { "operator", "keyword2", "operator" } + pattern = "^[%w%d]+%g+()%s*():()%s", + type = { "literal", "normal", "operator", "normal" } }, - { - pattern = "[%s][%w%d]+%g+%s*():%s", - type = { "keyword2", "operator" } - }, - { - pattern = "[%w%d]+%g+%s*():%s", - type = { "literal", "operator" } - }, - { pattern = "0%d+", type = "number" }, - { pattern = "0x%x+", type = "number" }, + -- variables + { pattern = "%$%a%w+", type = "keyword" }, + { pattern = "%$%{%{.-%}%}", type = "keyword" }, + -- numbers + { pattern = "[%+%-]?0%d+", type = "number" }, + { pattern = "[%+%-]?0x%x+", type = "number" }, { pattern = "[%+%-]?%d+[,%.eE:%+%d]*%d+", type = "number" }, + { pattern = "[%+%-]?%d+", type = "number" }, + -- special operators { pattern = "[%*%|%!>%%]", type = "keyword" }, - { pattern = "[%-:%?%*%{%}%[%]]", type = "operator" }, - -- Everything else for keywords to work - { - pattern = "[%d%a_][%g_]*()[%]%},]", - type = { "string", "operator" } - }, + { pattern = "[%-%$:%?]+", type = "operator" }, + -- Everything else as a string { pattern = "[%d%a_][%g_]*", type = "string" }, + { pattern = "%p+", type = "string" } }, symbols = { ["true"] = "number", diff --git a/plugins/language_zig.lua b/plugins/language_zig.lua index 192b114..f2c32d5 100644 --- a/plugins/language_zig.lua +++ b/plugins/language_zig.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/lfautoinsert.lua b/plugins/lfautoinsert.lua index a156964..ae3da75 100644 --- a/plugins/lfautoinsert.lua +++ b/plugins/lfautoinsert.lua @@ -1,11 +1,11 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local common = require "core.common" local config = require "core.config" local keymap = require "core.keymap" -config.plugins.lfautoinsert = { map = { +config.plugins.lfautoinsert = common.merge({ map = { ["{%s*\n"] = "}", ["%(%s*\n"] = ")", ["%f[[]%[%s*\n"] = "]", @@ -37,7 +37,7 @@ config.plugins.lfautoinsert = { map = { ["%[%[%s*\n"] = "]]" } }, -} } +} }, config.plugins.lfautoinsert) local function get_autoinsert_map(filename) local map = {} @@ -64,7 +64,7 @@ local function indent_size(doc, line) return e - s end -command.add("core.docview", { +command.add("core.docview!", { ["autoinsert:newline"] = function() command.perform("doc:newline") @@ -97,7 +97,7 @@ command.add("core.docview", { }) keymap.add { - ["return"] = { "command:submit", "autoinsert:newline" } + ["return"] = { "autoinsert:newline" } } return { diff --git a/plugins/linecopypaste.lua b/plugins/linecopypaste.lua deleted file mode 100644 index ea2a84c..0000000 --- a/plugins/linecopypaste.lua +++ /dev/null @@ -1,50 +0,0 @@ --- mod-version:2 -- lite-xl 2.0
-local core = require "core"
-local command = require "core.command"
-
-local function doc()
- return core.active_view.doc
-end
-
-local line_in_clipboard = false
-
-local doc_copy = command.map["doc:copy"].perform
-command.map["doc:copy"].perform = function()
- if doc():has_selection() then
- doc_copy()
- line_in_clipboard = false
- else
- local line = doc():get_selection()
- system.set_clipboard(doc().lines[line])
- line_in_clipboard = true
- end
-end
-
-local doc_cut = command.map["doc:cut"].perform
-command.map["doc:cut"].perform = function()
- if doc():has_selection() then
- doc_cut()
- line_in_clipboard = false
- else
- local line = doc():get_selection()
- system.set_clipboard(doc().lines[line])
- if line < #(doc().lines) then
- doc():remove(line, 1, line+1, 1)
- else -- last line in file
- doc():remove(line, 1, line, #(doc().lines[line]))
- end
- doc():set_selection(line, 1)
- line_in_clipboard = true
- end
-end
-
-local doc_paste = command.map["doc:paste"].perform
-command.map["doc:paste"].perform = function()
- if line_in_clipboard == false then
- doc_paste()
- else
- local line, col = doc():get_selection()
- doc():insert(line, 1, system.get_clipboard():gsub("\r", ""))
- doc():set_selection(line+1, col)
- end
-end
diff --git a/plugins/linenumbers.lua b/plugins/linenumbers.lua index 1b0bdc6..c075776 100644 --- a/plugins/linenumbers.lua +++ b/plugins/linenumbers.lua @@ -1,85 +1,116 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local config = require "core.config" local style = require "core.style" local DocView = require "core.docview" local common = require "core.common" local command = require "core.command" -local draw = DocView.draw_line_gutter -local get_width = DocView.get_gutter_width +config.plugins.linenumbers = common.merge({ + show = true, + relative = false, + -- The config specification used by the settings gui + config_spec = { + name = "Line Numbers", + { + label = "Show Numbers", + description = "Display or hide the line numbers.", + path = "show", + type = "toggle", + default = true + }, + { + label = "Relative Line Numbers", + description = "Display relative line numbers starting from active line.", + path = "relative", + type = "toggle", + default = false + } + } +}, config.plugins.linenumbers) -function DocView:draw_line_gutter(idx, x, y, width) - if not config.line_numbers and not config.relative_line_numbers then - return - end +local draw_line_gutter = DocView.draw_line_gutter +local get_gutter_width = DocView.get_gutter_width - if config.relative_line_numbers then +function DocView:draw_line_gutter(line, x, y, width) + local lh = self:get_line_height() + if not config.plugins.linenumbers.show then + return lh + end + if config.plugins.linenumbers.relative then local color = style.line_number - local local_idx = idx - local align = "right" + local local_idx = line - local l1 = self.doc:get_selection(false) - if idx == l1 then - color = style.line_number2 - if config.line_numbers then - align = "center" - else - local_idx = 0 + for _, line1, _, line2 in self.doc:get_selections(true) do + if line >= line1 and line <= line2 then + color = style.line_number2 + break end - else - local_idx = math.abs(idx - l1) end - -- Fix for old version (<=1.16) - if width == nil then - local gpad = style.padding.x * 2 - local gw = self:get_font():get_width(#self.doc.lines) + gpad - width = gpad and gw - gpad or gw + local l1 = self.doc:get_selection(false) + if line == l1 then + color = style.line_number2 + local_idx = 0 + else + local_idx = math.abs(line - l1) end common.draw_text( self:get_font(), - color, local_idx, align, + color, local_idx, "right", x + style.padding.x, - y + self:get_line_text_y_offset(), - width, self:get_line_height() + y, + width, lh ) else - draw(self, idx, x, y, width) + return draw_line_gutter(self, line, x, y, width) end + return lh end function DocView:get_gutter_width(...) - if not config.line_numbers and not config.relative_line_numbers then - return style.padding.x + if + not config.plugins.linenumbers.show + then + local width = get_gutter_width(self, ...) + + local correct_width = self:get_font():get_width(#self.doc.lines) + + (style.padding.x * 2) + + -- compatibility with center doc + if width <= correct_width then + width = style.padding.x + end + + return width, 0 else - return get_width(self, ...) + return get_gutter_width(self, ...) end end command.add(nil, { ["line-numbers:toggle"] = function() - config.line_numbers = not config.line_numbers + config.plugins.linenumbers.show = not config.plugins.linenumbers.show end, ["line-numbers:disable"] = function() - config.line_numbers = false + config.plugins.linenumbers.show = false end, ["line-numbers:enable"] = function() - config.line_numbers = true + config.plugins.linenumbers.show = true end, ["relative-line-numbers:toggle"] = function() - config.relative_line_numbers = not config.relative_line_numbers + config.plugins.linenumbers.relative = not config.plugins.linenumbers.relative end, ["relative-line-numbers:enable"] = function() - config.relative_line_numbers = true + config.plugins.linenumbers.relative = true end, ["relative-line-numbers:disable"] = function() - config.relative_line_numbers = false + config.plugins.linenumbers.relative = false end }) diff --git a/plugins/macmodkeys.lua b/plugins/macmodkeys.lua index d8c0d05..e4656b9 100644 --- a/plugins/macmodkeys.lua +++ b/plugins/macmodkeys.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local keymap = require "core.keymap" local on_key_pressed = keymap.on_key_pressed diff --git a/plugins/markers.lua b/plugins/markers.lua index ad89fad..e2ec68e 100644 --- a/plugins/markers.lua +++ b/plugins/markers.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0
+-- mod-version:3
-- Markers plugin for lite text editor
-- original implementation by Petri Häkkinen
@@ -52,12 +52,12 @@ end local draw_line_gutter = DocView.draw_line_gutter
-function DocView:draw_line_gutter(idx, x, y, width)
- if cache[self.doc] and cache[self.doc][idx] then
+function DocView:draw_line_gutter(line, x, y, width)
+ if cache[self.doc] and cache[self.doc][line] then
local h = self:get_line_height()
renderer.draw_rect(x, y, style.padding.x * 0.4, h, style.selection)
end
- draw_line_gutter(self, idx, x, y, width)
+ return draw_line_gutter(self, line, x, y, width)
end
diff --git a/plugins/memoryusage.lua b/plugins/memoryusage.lua index fcdcc29..290b4e1 100644 --- a/plugins/memoryusage.lua +++ b/plugins/memoryusage.lua @@ -1,19 +1,49 @@ --- mod-version:2 -- lite-xl 2.0 +-- 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" -local get_items = StatusView.get_items - -function StatusView:get_items() - local left, right = get_items(self) - local t = { - style.text, (math.floor(collectgarbage("count") / 10.24) / 100) .. " MB", - style.dim, self.separator2, +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 + } } - for i, item in ipairs(t) do - table.insert(right, i, item) - end - return left, right -end +}, config.plugins.memoryusage) + +core.status_view:add_item({ + name = "status:memory-usage", + alignment = StatusView.Item.RIGHT, + get_item = function() + return { + style.text, + string.format( + "%.2f MB", + (math.floor(collectgarbage("count") / 10.24) / 100) + ) + } + end, + position = 1, + tooltip = "lua memory usage", + separator = core.status_view.separator2 +}) diff --git a/plugins/minimap.lua b/plugins/minimap.lua index 4bf4fca..c2dd8f0 100644 --- a/plugins/minimap.lua +++ b/plugins/minimap.lua @@ -1,382 +1,666 @@ --- mod-version:2 +-- mod-version:3 local core = require "core" local command = require "core.command" local common = require "core.common" local config = require "core.config" local style = require "core.style" local DocView = require "core.docview" +local Highlighter = require "core.doc.highlighter" local Object = require "core.object" +local Scrollbar = require "core.scrollbar" + +-- Sample configurations: +-- full width: +-- config.plugins.minimap.highlight_width = 100 +-- config.plugins.minimap.gutter_width = 0 +-- left side: +-- config.plugins.minimap.highlight_align = 'left' +-- config.plugins.minimap.highlight_width = 3 +-- config.plugins.minimap.gutter_width = 4 +-- right side: +-- config.plugins.minimap.highlight_align = 'right' +-- config.plugins.minimap.highlight_width = 5 +-- config.plugins.minimap.gutter_width = 0 -- General plugin settings -config.plugins.minimap = { - enabled = true, - width = 100, - instant_scroll = false, - syntax_highlight = true, - scale = 1, - -- how many spaces one tab is equivalent to - tab_width = 4, - draw_background = true, - - -- you can override these colors - selection_color = nil, - caret_color = nil, - - -- If other plugins provide per-line highlights, - -- this controls the placement. (e.g. gitdiff_highlight) - highlight_align = 'left', - highlight_width = 3, - gutter_width = 5, - -- try these values: - -- full width: - -- config.plugins.minimap.highlight_width = 100 - -- config.plugins.minimap.gutter_width = 0 - -- left side: - -- config.plugins.minimap.highlight_align = 'left' - -- config.plugins.minimap.highlight_width = 3 - -- config.plugins.minimap.gutter_width = 4 - -- right side: - -- config.plugins.minimap.highlight_align = 'right' - -- config.plugins.minimap.highlight_width = 5 - -- config.plugins.minimap.gutter_width = 0 +config.plugins.minimap = common.merge({ + enabled = true, + width = 100, + instant_scroll = false, + syntax_highlight = true, + scale = 1, + -- number of spaces needed to split a token + spaces_to_split = 2, + -- hide on small docs (can be true, false or min number of lines) + avoid_small_docs = false, + -- how many spaces one tab is equivalent to + tab_width = 4, + draw_background = true, + -- you can override these colors + selection_color = nil, + caret_color = nil, + -- If other plugins provide per-line highlights, + -- this controls the placement. (e.g. gitdiff_highlight) + highlight_align = 'left', + highlight_width = 3, + gutter_width = 5, + -- The config specification used by the settings gui + config_spec = { + name = "Mini Map", + { + label = "Enabled", + description = "Activate the minimap by default.", + path = "enabled", + type = "toggle", + default = true + }, + { + label = "Width", + description = "Width of the minimap in pixels.", + path = "width", + type = "number", + default = 100, + min = 50, + max = 1000 + }, + { + label = "Instant Scroll", + description = "When enabled disables the scrolling animation.", + path = "instant_scroll", + type = "toggle", + default = false + }, + { + label = "Syntax Highlighting", + description = "Disable to improve performance.", + path = "syntax_highlight", + type = "toggle", + default = true + }, + { + label = "Scale", + description = "Size of the minimap using a scaling factor.", + path = "scale", + type = "number", + default = 1, + min = 0.5, + max = 10, + step = 0.1 + }, + { + label = "Spaces to split", + description = "Number of spaces needed to split a token.", + path = "spaces_to_split", + type = "number", + default = 2, + min = 1 + }, + { + label = "Hide for small Docs", + description = "Hide the minimap when a Doc is small enough.", + path = "avoid_small_docs", + type = "toggle", + default = false + }, + { + label = "Small Docs definition", + description = "Size of a Doc to be considered small. Use 0 to automatically decide.", + path = "avoid_small_docs_len", + type = "number", + default = 0, + min = 0, + on_apply = function(value) + if value == 0 then + config.plugins.minimap.avoid_small_docs = true + else + config.plugins.minimap.avoid_small_docs = value + end + end + }, + { + label = "Tabs Width", + description = "The amount of spaces that represent a tab.", + path = "tab_width", + type = "number", + default = 4, + min = 1, + max = 8 + }, + { + label = "Draw Background", + description = "When disabled makes the minimap transparent.", + path = "draw_background", + type = "toggle", + default = true + }, + { + 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 + }, + { + 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 + }, + { + label = "Highlight Alignment", + path = "highlight_align", + type = "selection", + default = "left", + values = { + {"Left", "left"}, + {"Right", "right"} + } + }, + { + label = "Highlight Width", + path = "highlight_width", + type = "number", + default = 3, + min = 0, + max = 50 + }, + { + label = "Gutter Width", + description = "Left padding of the minimap.", + path = "gutter_width", + type = "number", + default = 5, + min = 0, + max = 50 + }, + } +}, config.plugins.minimap) + + +-- contains the settings values that require a cache reset if changed +local cached_settings = { + color_scheme_canary = nil, + syntax_highlight = nil, + spaces_to_split = nil, + scale = nil, + width = nil, } -- Configure size for rendering each char in the minimap -local char_height = 1 * SCALE * config.plugins.minimap.scale -local char_spacing = 0.8 * SCALE * config.plugins.minimap.scale -local line_spacing = 2 * SCALE * config.plugins.minimap.scale +local char_spacing +local char_height +local line_spacing + +-- cache for the location of the rects for each Doc +local highlighter_cache +local function reset_cache() + highlighter_cache = setmetatable({}, { __mode = "k" }) + cached_settings = { + color_scheme_canary = style.syntax["normal"], + syntax_highlight = config.plugins.minimap.syntax_highlight, + spaces_to_split = config.plugins.minimap.spaces_to_split, + scale = config.plugins.minimap.scale, + width = config.plugins.minimap.width, + } + char_spacing = 0.8 * SCALE * config.plugins.minimap.scale + -- keep y aligned to pixels + char_height = math.max(1, math.floor(1 * SCALE * config.plugins.minimap.scale + 0.5)) + line_spacing = math.max(1, math.floor(2 * SCALE * config.plugins.minimap.scale + 0.5)) +end +reset_cache() + + +local function reset_cache_if_needed() + if + cached_settings.color_scheme_canary ~= style.syntax["normal"] + or cached_settings.syntax_highlight ~= config.plugins.minimap.syntax_highlight + or cached_settings.spaces_to_split ~= config.plugins.minimap.spaces_to_split + or cached_settings.scale ~= config.plugins.minimap.scale + or cached_settings.width ~= config.plugins.minimap.width + then + reset_cache() + end +end + + + + +-- Move cache to make space for new lines +local prev_insert_notify = Highlighter.insert_notify +function Highlighter:insert_notify(line, n, ...) + prev_insert_notify(self, line, n, ...) + local blanks = { } + 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 + end + end +end + + +-- Close the cache gap created by removed lines +local prev_remove_notify = Highlighter.remove_notify +function Highlighter:remove_notify(line, n, ...) + prev_remove_notify(self, 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 + end +end + + +-- Remove changed lines from the cache +local prev_tokenize_line = Highlighter.tokenize_line +function Highlighter:tokenize_line(idx, state, ...) + local res = prev_tokenize_line(self, idx, state, ...) + if not highlighter_cache[self] then + highlighter_cache[self] = {} + end + highlighter_cache[self][idx] = nil + return res +end + +-- Ask the Highlighter to retokenize the lines we have in cache +local prev_invalidate = Highlighter.invalidate +function Highlighter:invalidate(idx, ...) + local cache = highlighter_cache[self] + if cache then + self.max_wanted_line = math.max(self.max_wanted_line, #cache) + end + return prev_invalidate(self, idx, ...) +end + + +-- Remove cache on Highlighter reset (for example on syntax change) +local prev_soft_reset = Highlighter.soft_reset +function Highlighter:soft_reset(...) + prev_soft_reset(self, ...) + highlighter_cache[self] = {} +end + + +local MiniMap = Scrollbar:extend() -local MiniMap = Object:extend() -function MiniMap:new() +function MiniMap:new(dv) + MiniMap.super.new(self, { direction = "v", alignment = "e" }) + self.dv = dv + self.enabled = nil end + function MiniMap:line_highlight_color(line_index) - -- other plugins can override this, and return a color + -- other plugins can override this, and return a color end -local minimap = MiniMap() -local function show_minimap() - return config.plugins.minimap.enabled - and getmetatable(core.active_view) == DocView - and core.active_view ~= core.command_view - and core.active_view.doc +function MiniMap:is_minimap_enabled() + if self.enabled ~= nil then return self.enabled end + if not config.plugins.minimap.enabled then return false end + if config.plugins.minimap.avoid_small_docs then + local last_line = #self.dv.doc.lines + if type(config.plugins.minimap.avoid_small_docs) == "number" then + return last_line > config.plugins.minimap.avoid_small_docs + else + local docview = self.dv + local _, y = docview:get_line_screen_position(last_line, docview.doc.lines[last_line]) + y = y + docview.scroll.y - docview.position.y + docview:get_line_height() + return y > docview.size.y + end + end + return true +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) + end + return visible_lines_start, visible_lines_count, minimap_lines_start, minimap_lines_count, is_file_too_large end --- Overloaded since the default implementation adds a extra x3 size of hotspot for the mouse to hit the scrollbar. -local prev_scrollbar_overlaps_point = DocView.scrollbar_overlaps_point -DocView.scrollbar_overlaps_point = function(self, x, y) - if not show_minimap() then - return prev_scrollbar_overlaps_point(self, x, y) - end - local sx, sy, sw, sh = self:get_scrollbar_rect() - return x >= sx and x < sx + sw and y >= sy and y < sy + sh +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 end --- Helper function to determine if current file is too large to be shown fully inside the minimap area. -local function is_file_too_large(self) - local line_count = #self.doc.lines - local _, _, _, sh = self:get_scrollbar_rect() - -- check if line count is too large to fit inside the minimap area - local max_minmap_lines = math.floor(sh / line_spacing) - return line_count > 1 and line_count > max_minmap_lines +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_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 + 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 end --- Overloaded with an extra check if the user clicked inside the minimap to automatically scroll to that line. -local prev_on_mouse_pressed = DocView.on_mouse_pressed -DocView.on_mouse_pressed = function(self, button, x, y, clicks) - if not show_minimap() then - return prev_on_mouse_pressed(self, button, x, y, clicks) - end - - -- check if user clicked in the minimap area and jump directly to that line - -- unless they are actually trying to perform a drag - local minimap_hit = self:scrollbar_overlaps_point(x, y) - if minimap_hit then - local line_count = #self.doc.lines - local minimap_height = line_count * line_spacing - - -- check if line count is too large to fit inside the minimap area - local is_too_large = is_file_too_large(self) - if is_too_large then - local _, _, _, sh = self:get_scrollbar_rect() - minimap_height = sh - end - - -- calc which line to jump to - local dy = y - self.position.y - local jump_to_line = math.floor((dy / minimap_height) * line_count) + 1 - - local _, cy, _, cy2 = self:get_content_bounds() - local lh = self:get_line_height() - local visible_lines_count = math.max(1, (cy2 - cy) / lh) - local visible_lines_start = math.max(1, math.floor(cy / lh)) - - -- calc if user hit the currently visible area - local hit_visible_area = true - if is_too_large then - - local visible_height = visible_lines_count * line_spacing - local scroll_pos = (visible_lines_start - 1) / - (line_count - visible_lines_count - 1) - scroll_pos = math.min(1.0, scroll_pos) -- 0..1 - local visible_y = self.position.y + scroll_pos * - (minimap_height - visible_height) - - local t = (line_count - visible_lines_start) / visible_lines_count - if t <= 1 then visible_y = visible_y + visible_height * (1.0 - t) end - - if y < visible_y or y > visible_y + visible_height then - hit_visible_area = false - end - else - - -- If the click is on the currently visible line numbers, - -- ignore it since then they probably want to initiate a drag instead. - if jump_to_line < visible_lines_start or jump_to_line > visible_lines_start + - visible_lines_count then hit_visible_area = false end - end - - -- if user didn't click on the visible area (ie not dragging), scroll accordingly - if not hit_visible_area then - self:scroll_to_line(jump_to_line, false, config.plugins.minimap.instant_scroll) - end - - end - - return prev_on_mouse_pressed(self, button, x, y, clicks) + +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) end --- Overloaded with pretty much the same logic as original DocView implementation, --- with the exception of the dragging scrollbar delta. We want it to behave a bit snappier --- since the "scrollbar" essentially represents the lines visible in the content view. -local prev_on_mouse_moved = DocView.on_mouse_moved -DocView.on_mouse_moved = function(self, x, y, dx, dy) - if not show_minimap() then - return prev_on_mouse_moved(self, x, y, dx, dy) - end - - if self.dragging_scrollbar then - local line_count = #self.doc.lines - local lh = self:get_line_height() - local delta = lh / line_spacing * dy - - if is_file_too_large(self) then - local _, sy, _, sh = self:get_scrollbar_rect() - delta = (line_count * lh) / sh * dy - end - - self.scroll.to.y = self.scroll.to.y + delta - end - - -- we need to "hide" that the scrollbar is dragging so that View doesnt does its own scrolling logic - local t = self.dragging_scrollbar - self.dragging_scrollbar = false - local r = prev_on_mouse_moved(self, x, y, dx, dy) - self.dragging_scrollbar = t - return r + +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 --- Overloaded since we want the mouse to interact with the full size of the minimap area, --- not juse the scrollbar. -local prev_get_scrollbar_rect = DocView.get_scrollbar_rect -DocView.get_scrollbar_rect = function(self) - if not show_minimap() then return prev_get_scrollbar_rect(self) 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 - return self.position.x + self.size.x - config.plugins.minimap.width * SCALE, - self.position.y, config.plugins.minimap.width * SCALE, self.size.y +function MiniMap:draw() + if not self:is_minimap_enabled() then return MiniMap.super.draw(self) end + local dv = self.dv + local x, y, w, h = self:get_track_rect() + + 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) + end + self:draw_thumb() + + -- 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 + local selection_min_y = math.min(selection1_y, selection2_y) + local selection_h = math.abs(selection2_y - selection1_y)+1 + renderer.draw_rect(x, selection_min_y, w, selection_h, selection_color) + renderer.draw_rect(x, selection1_y, w, line_spacing, caret_color) + end + + local highlight_align = config.plugins.minimap.highlight_align + local highlight_width = config.plugins.minimap.highlight_width + local gutter_width = config.plugins.minimap.gutter_width + + -- time to draw the actual code, setup some local vars that are used in both highlighted and plain rendering. + local line_y = y + + -- when not using syntax highlighted rendering, just use the normal color but dim it 50%. + local color = style.syntax["normal"] + color = {color[1], color[2], color[3], color[4] * 0.5} + + -- we try to "batch" characters so that they can be rendered as just one rectangle instead of one for each. + local batch_width = 0 + local batch_start = x + local last_batch_end = -1 + local minimap_cutoff_x = config.plugins.minimap.width * SCALE + local batch_syntax_type = nil + local function flush_batch(type, cache) + if batch_width > 0 then + local lastidx = #cache + local old_color = color + color = style.syntax[type] + if config.plugins.minimap.syntax_highlight and color ~= nil then + -- fetch and dim colors + color = {color[1], color[2], color[3], (color[4] or 255) * 0.5} + else + color = old_color + end + if #cache >= 3 then + local last_color = cache[lastidx] + if + last_batch_end == batch_start -- no space skipped + and ( + batch_syntax_type == type -- and same syntax + or ( -- or same color + last_color[1] == color[1] + and last_color[2] == color[2] + and last_color[3] == color[3] + and last_color[4] == color[4] + ) + ) + then + batch_start = cache[lastidx - 2] + batch_width = cache[lastidx - 1] + batch_width + lastidx = lastidx - 3 + end + end + cache[lastidx + 1] = batch_start + cache[lastidx + 2] = batch_width + cache[lastidx + 3] = color + end + batch_syntax_type = type + batch_start = batch_start + batch_width + last_batch_end = batch_start + batch_width = 0 + end + + local highlight_x + if highlight_align == 'left' then + highlight_x = x + else + highlight_x = x + w - highlight_width + end + local function render_highlight(idx, line_y) + local highlight_color = self:line_highlight_color(idx) + if highlight_color then + renderer.draw_rect(highlight_x, line_y, highlight_width, line_spacing, 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 + + -- per line + for idx = minimap_lines_start, endidx do + batch_syntax_type = nil + batch_start = 0 + batch_width = 0 + last_batch_end = -1 + + render_highlight(idx, line_y) + local cache = highlighter_cache[dv.doc.highlighter][idx] + if not highlighter_cache[dv.doc.highlighter][idx] then -- need to cache + highlighter_cache[dv.doc.highlighter][idx] = {} + cache = highlighter_cache[dv.doc.highlighter][idx] + -- per token + for _, type, text in dv.doc.highlighter:each_token(idx) do + if not config.plugins.minimap.syntax_highlight then + type = nil + end + local start = 1 + while true do + -- find text followed spaces followed by newline + local s, e, w, eol = string.ufind(text, "[^%s]*()[ \t]*()\n?", start) + if not s then break end + local nchars = w - s + start = e + 1 + batch_width = batch_width + char_spacing * nchars + + local nspaces = 0 + for i=w,e do + local whitespace = string.sub(text, i, i) + if whitespace == "\t" then + nspaces = nspaces + config.plugins.minimap.tab_width + elseif whitespace == " " then + nspaces = nspaces + 1 + end + end + -- not enough spaces; consider them part of the batch + if nspaces < config.plugins.minimap.spaces_to_split then + batch_width = batch_width + nspaces * char_spacing + end + -- line has ended or no more space in the minimap; + -- we can go to the next line + if eol <= w or batch_start + batch_width > minimap_cutoff_x then + if batch_width > 0 then + flush_batch(type, cache) + end + break + end + -- enough spaces to split the batch + if nspaces >= config.plugins.minimap.spaces_to_split then + flush_batch(type, cache) + batch_start = batch_start + nspaces * char_spacing + end + end + end + end + -- draw from cache + for i=1,#cache,3 do + local batch_start = cache[i ] + x + gutter_width + local batch_width = cache[i + 1] + local color = cache[i + 2] + renderer.draw_rect(batch_start, line_y, batch_width, char_height, color) + end + line_y = line_y + line_spacing + end end --- Overloaded so we can render the minimap in the "scrollbar area". -local prev_draw_scrollbar = DocView.draw_scrollbar -DocView.draw_scrollbar = function(self) - if not show_minimap() then return prev_draw_scrollbar(self) end - - local x, y, w, h = self:get_scrollbar_rect() - - local highlight = self.hovered_scrollbar or self.dragging_scrollbar - local visual_color = highlight and style.scrollbar2 or style.scrollbar - - local _, cy, _, cy2 = self:get_content_bounds() - local lh = self:get_line_height() - local visible_lines_count = math.max(1, (cy2 - cy) / lh) - local visible_lines_start = math.max(1, math.floor(cy / lh)) - local scroller_height = visible_lines_count * line_spacing - local line_count = #self.doc.lines - - local visible_y = self.position.y + (visible_lines_start - 1) * line_spacing - - -- check if file is too large to fit inside the minimap area - local max_minmap_lines = math.floor(h / line_spacing) - local minimap_start_line = 1 - if is_file_too_large(self) 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 scroll_pos_pixels = scroll_pos * (h - scroller_height) - visible_y = self.position.y + scroll_pos_pixels - - -- offset visible area if user is scrolling past end - local t = (line_count - visible_lines_start) / visible_lines_count - if t <= 1 then visible_y = visible_y + scroller_height * (1.0 - t) end - - minimap_start_line = visible_lines_start - - math.floor(scroll_pos_pixels / line_spacing) - minimap_start_line = math.max(1, math.min(minimap_start_line, - line_count - max_minmap_lines)) - end - - if config.plugins.minimap.draw_background then - renderer.draw_rect(x, y, w, h, style.minimap_background or style.background) - end - -- draw visual rect - renderer.draw_rect(x, visible_y, w, scroller_height, visual_color) - - -- 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 - local selection_line, selection_col, selection_line2, selection_col2 = self.doc:get_selection() - local selection_y = y + (selection_line - minimap_start_line) * line_spacing - local selection2_y = y + (selection_line2 - minimap_start_line) * line_spacing - local selection_min_y = math.min(selection_y, selection2_y) - local selection_h = math.abs(selection2_y - selection_y)+1 - renderer.draw_rect(x, selection_min_y, w, selection_h, selection_color) - renderer.draw_rect(x, selection_y, w, line_spacing, caret_color) - - local highlight_align = config.plugins.minimap.highlight_align - local highlight_width = config.plugins.minimap.highlight_width - local gutter_width = config.plugins.minimap.gutter_width - - -- time to draw the actual code, setup some local vars that are used in both highlighted and plain renderind. - local line_y = y - - -- when not using syntax highlighted rendering, just use the normal color but dim it 50%. - local color = style.syntax["normal"] - color = {color[1], color[2], color[3], color[4] * 0.5} - - -- we try to "batch" characters so that they can be rendered as just one rectangle instead of one for each. - local batch_width = 0 - local batch_start = x - local minimap_cutoff_x = x + config.plugins.minimap.width * SCALE - local batch_syntax_type = nil - local function flush_batch(type) - local old_color = color - color = style.syntax[batch_syntax_type] - if config.plugins.minimap.syntax_highlight and color ~= nil then - -- fetch and dim colors - color = {color[1], color[2], color[3], color[4] * 0.5} - else - color = old_color - end - if batch_width > 0 then - renderer.draw_rect(batch_start, line_y, batch_width, char_height, color) - end - batch_syntax_type = type - batch_start = batch_start + batch_width - batch_width = 0 - end - - local highlight_x - if highlight_align == 'left' then - highlight_x = x - else - highlight_x = x + w - highlight_width - end - local function render_highlight(idx, line_y) - local highlight_color = minimap:line_highlight_color(idx) - if highlight_color then - renderer.draw_rect(highlight_x, line_y, highlight_width, line_spacing, highlight_color) - end - end - - -- render lines with syntax highlighting - if config.plugins.minimap.syntax_highlight then - - -- keep track of the highlight type, since this needs to break batches as well - batch_syntax_type = nil - - -- per line - local endidx = minimap_start_line + max_minmap_lines - endidx = math.min(endidx, line_count) - for idx = minimap_start_line, endidx do - batch_syntax_type = nil - batch_start = x + gutter_width - batch_width = 0 - - render_highlight(idx, line_y) - - -- per token - for _, type, text in self.doc.highlighter:each_token(idx) do - -- flush prev batch - if not batch_syntax_type then batch_syntax_type = type end - if batch_syntax_type ~= type then flush_batch(type) end - - -- per character - for char in common.utf8_chars(text) do - if char == " " or char == "\n" then - flush_batch(type) - batch_start = batch_start + char_spacing - elseif char == " " then - flush_batch(type) - batch_start = batch_start + (char_spacing * config.plugins.minimap.tab_width) - elseif batch_start + batch_width > minimap_cutoff_x then - flush_batch(type) - break - else - batch_width = batch_width + char_spacing - end - - end - end - flush_batch(nil) - line_y = line_y + line_spacing - end - - else -- render lines without syntax highlighting - for idx = 1, line_count - 1 do - batch_start = x + gutter_width - batch_width = 0 - - render_highlight(idx, line_y) - - for char in common.utf8_chars(self.doc.lines[idx]) do - if char == " " or char == "\n" then - flush_batch() - batch_start = batch_start + char_spacing - elseif batch_start + batch_width > minimap_cutoff_x then - flush_batch() - else - batch_width = batch_width + char_spacing - end - end - flush_batch() - line_y = line_y + line_spacing - end - - end +local old_docview_new = DocView.new +function DocView:new(doc) + old_docview_new(self, doc) + if self:is(DocView) then self.v_scrollbar = MiniMap(self) end +end + +local old_docview_scroll_to_make_visible = DocView.scroll_to_make_visible +function DocView:scroll_to_make_visible(line, col, ...) + if + not self:is(DocView) or not self.v_scrollbar:is(MiniMap) + or + not self.v_scrollbar:is_minimap_enabled() + then + return old_docview_scroll_to_make_visible(self, line, col, ...) + 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 -local prev_update = DocView.update -DocView.update = function (self) - if not show_minimap() then return prev_update(self) end - self.size.x = self.size.x - config.plugins.minimap.width * SCALE - return prev_update(self) + +local function get_all_docviews(node, t) + t = t or {} + if not node then return end + if node.type == "leaf" then + for i,v in ipairs(node.views) do + if v:is(DocView) then + table.insert(t, v) + end + end + end + get_all_docviews(node.a, t) + get_all_docviews(node.b, t) + return t end + command.add(nil, { - ["minimap:toggle-visibility"] = function() - config.plugins.minimap.enabled = not config.plugins.minimap.enabled - end, - ["minimap:toggle-syntax-highlighting"] = function() - config.plugins.minimap.syntax_highlight = not config.plugins.minimap.syntax_highlight - end + ["minimap:toggle-visibility"] = function() + config.plugins.minimap.enabled = not config.plugins.minimap.enabled + for i,v in ipairs(get_all_docviews(core.root_view.root_node)) do + v.v_scrollbar.enabled = nil + end + end, + ["minimap:toggle-syntax-highlighting"] = function() + config.plugins.minimap.syntax_highlight = not config.plugins.minimap.syntax_highlight + end +}) + +command.add("core.docview!", { + ["minimap:toggle-visibility-for-current-view"] = function() + local sb = core.active_view.v_scrollbar + if sb.enabled ~= nil then + sb.enabled = not sb.enabled + else + sb.enabled = not config.plugins.minimap.enabled + end + end }) -return minimap +return MiniMap diff --git a/plugins/motiontrail.lua b/plugins/motiontrail.lua index 1359c90..16e7307 100644 --- a/plugins/motiontrail.lua +++ b/plugins/motiontrail.lua @@ -1,10 +1,34 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local config = require "core.config" +local common = require "core.common" local style = require "core.style" local DocView = require "core.docview" -config.plugins.motiontrail = { steps = 50 } +config.plugins.motiontrail = common.merge({ + enabled = true, + steps = 50, + -- The config specification used by the settings gui + config_spec = { + name = "Motion Trail", + { + label = "Enabled", + description = "Disable or enable the caret motion trail effect.", + path = "enabled", + type = "toggle", + default = true + }, + { + label = "Steps", + description = "Amount of trail steps to generate on caret movement.", + path = "steps", + type = "number", + default = 50, + min = 10, + max = 100 + }, + } +}, config.plugins.motiontrail) local function lerp(a, b, t) @@ -14,8 +38,7 @@ end local function get_caret_rect(dv) local line, col = dv.doc:get_selection() - local x, y = dv:get_line_screen_position(line) - x = x + dv:get_col_x_offset(line, col) + local x, y = dv:get_line_screen_position(line, col) return x, y, style.caret_width, dv:get_line_height() end @@ -26,7 +49,9 @@ local draw = DocView.draw function DocView:draw(...) draw(self, ...) - if self ~= core.active_view then return end + if not config.plugins.motiontrail.enabled or self ~= core.active_view then + return + end local x, y, w, h = get_caret_rect(self) diff --git a/plugins/navigate.lua b/plugins/navigate.lua index 4e6092f..d83c02f 100644 --- a/plugins/navigate.lua +++ b/plugins/navigate.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local common = require "core.common" diff --git a/plugins/nonicons.lua b/plugins/nonicons.lua index 9a1e963..b8b01bc 100644 --- a/plugins/nonicons.lua +++ b/plugins/nonicons.lua @@ -1,11 +1,57 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 +-- Author: Jipok +-- Doesn't work well with scaling mode == "ui" + local core = require "core" local common = require "core.common" +local config = require "core.config" local style = require "core.style" local TreeView = require "plugins.treeview" +local Node = require "core.node" +-- Config +config.plugins.nonicons = common.merge({ + use_default_dir_icons = false, + use_default_chevrons = false, + draw_treeview_icons = true, + draw_tab_icons = true, + -- The config specification used by the settings gui + config_spec = { + name = "Nonicons", + { + label = "Use Default Directory Icons", + description = "When enabled does not use nonicon directory icons.", + path = "use_default_dir_icons", + type = "toggle", + default = false + }, + { + label = "Use Default Chevrons", + description = "When enabled does not use nonicon expand/collapse arrow icons.", + path = "use_default_chevrons", + type = "toggle", + default = false + }, + { + label = "Draw Treeview Icons", + description = "Enables file related icons on the treeview.", + path = "draw_treeview_icons", + type = "toggle", + default = true + }, + { + label = "Draw Tab Icons", + description = "Adds file related icons to tabs.", + path = "draw_tab_icons", + type = "toggle", + default = true + } + } +}, config.plugins.nonicons) local icon_font = renderer.font.load(USERDIR.."/fonts/nonicons.ttf", 15 * SCALE) +local chevron_width = icon_font:get_width("") +local previous_scale = SCALE local extension_icons = { [".lua"] = { "#51a0cf", "" }, [".md"] = { "#519aba", "" }, -- Markdown @@ -39,16 +85,16 @@ local extension_icons = { [".swift"] = { "#e37933", "" }, [".ts"] = { "#519aba", "" }, -- TypeScript [".elm"] = { "#519aba", "" }, - [".diff"] = { "#41535b", "" }, [".patch"] = { "#41535b", "" }, + [".diff"] = { "#41535b", "" }, [".ex"] = { "#a074c4", "" }, [".exs"] = { "#a074c4", "" }, -- Elixir -- Following without special icon: [".awk"] = { "#4d5a5e", "" }, [".nim"] = { "#F88A02", "" }, [".zig"] = { "#cbcb41", "" }, - } local known_names_icons = { ["changelog"] = { "#657175", "" }, ["changelog.txt"] = { "#4d5a5e", "" }, + ["changelog.md"] = { "#519aba", "" }, ["makefile"] = { "#6d8086", "" }, ["dockerfile"] = { "#296478", "" }, ["docker-compose.yml"] = { "#4289a1", "" }, @@ -68,66 +114,68 @@ for k, v in pairs(known_names_icons) do v[1] = { common.color(v[1]) } end --- Replace original draw -function TreeView:draw() - if not self.visible then return end - self:draw_background(style.background2) - - local icon_width = icon_font:get_width("") - local spacing = icon_font:get_width("") / 2 - - local doc = core.active_view.doc - local active_filename = doc and system.absolute_path(doc.filename or "") - - for item, x,y,w,h in self:each_item() do - local color = style.text - - -- highlight active_view doc - if item.abs_filename == active_filename then - color = style.accent +-- Override function to change default icons for dirs, special extensions and names +local TreeView_get_item_icon = TreeView.get_item_icon +function TreeView:get_item_icon(item, active, hovered) + local icon, font, color = TreeView_get_item_icon(self, item, active, hovered) + if previous_scale ~= SCALE then + icon_font:set_size( + icon_font:get_size() * (SCALE / previous_scale) + ) + chevron_width = icon_font:get_width("") + previous_scale = SCALE + end + if not config.plugins.nonicons.use_default_dir_icons then + icon = "" -- unicode 61766 + font = icon_font + color = style.text + if item.type == "dir" then + icon = item.expanded and "" or "" -- unicode U+F23C and U+F23B end - - -- hovered item background - if item == self.hovered_item then - renderer.draw_rect(x, y, w, h, style.line_highlight) + end + if config.plugins.nonicons.draw_treeview_icons then + local custom_icon = known_names_icons[item.name:lower()] + if custom_icon == nil then + custom_icon = extension_icons[item.name:match("^.+(%..+)$")] + end + if custom_icon ~= nil then + color = custom_icon[1] + icon = custom_icon[2] + font = icon_font + end + if active or hovered then color = style.accent end + end + return icon, font, color +end - -- icons - x = x + item.depth * style.padding.x + style.padding.x +-- Override function to draw chevrons if setting is disabled +local TreeView_draw_item_chevron = TreeView.draw_item_chevron +function TreeView:draw_item_chevron(item, active, hovered, x, y, w, h) + if not config.plugins.nonicons.use_default_chevrons then if item.type == "dir" then - local icon1 = item.expanded and "" or "" -- unicode 61726 and 61728 - local icon2 = item.expanded and "" or "" -- unicode U+F23C and U+F23B - x = x - spacing - common.draw_text(icon_font, color, icon1, nil, x, y, 0, h) - x = x + style.padding.x + spacing - common.draw_text(icon_font, color, icon2, nil, x, y, 0, h) - x = x + icon_width - else - x = x + style.padding.x - -- default icon - local icon = "" -- unicode 61766 - local icon_color = color - -- icon depending on the file extension or full name - local custom_icon = known_names_icons[item.name:lower()] - if custom_icon == nil then - custom_icon = extension_icons[item.name:match("^.+(%..+)$")] - end - if custom_icon ~= nil then - icon_color = custom_icon[1] - icon = custom_icon[2] - end - common.draw_text(icon_font, icon_color, icon, nil, x, y, 0, h) - x = x + icon_width + local chevron_icon = item.expanded and "" or "" + local chevron_color = hovered and style.accent or style.text + common.draw_text(icon_font, chevron_color, chevron_icon, nil, x, y, 0, h) end - - -- text - x = x + spacing - x = common.draw_text(style.font, color, item.name, nil, x, y, 0, h) + return chevron_width + style.padding.x/4 end + return TreeView_draw_item_chevron(self, item, active, hovered, x, y, w, h) +end - self:draw_scrollbar() - if self.hovered_item and self.tooltip.alpha > 0 then - core.root_view:defer_draw(self.draw_tooltip, self) +-- Override function to draw icons in tabs titles if setting is enabled +local Node_draw_tab_title = Node.draw_tab_title +function Node:draw_tab_title(view, font, is_active, is_hovered, x, y, w, h) + if config.plugins.nonicons.draw_tab_icons then + local padx = chevron_width + style.padding.x/2 + local tx = x + padx -- Space for icon + w = w - padx + Node_draw_tab_title(self, view, font, is_active, is_hovered, tx, y, w, h) + if (view == nil) or (view.doc == nil) then return end + local item = { type = "file", name = view.doc:get_name() } + TreeView:draw_item_icon(item, false, is_hovered, x, y, w, h) + else + Node_draw_tab_title(self, view, font, is_active, is_hovered, x, y, w, h) end end diff --git a/plugins/opacity.lua b/plugins/opacity.lua index 8dd0d9a..a97cae9 100644 --- a/plugins/opacity.lua +++ b/plugins/opacity.lua @@ -1,4 +1,5 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 +local core = require "core" local common = require "core.common" local command = require "core.command" local keymap = require "core.keymap" @@ -11,7 +12,7 @@ local default_opacity = 1 local current_opacity = default_opacity local function set_opacity(opacity) - if not opacity_on then opacity_on = true end + if not opacity_on then return end current_opacity = common.clamp(opacity, 0.2, 1) system.set_window_opacity(current_opacity) end @@ -30,8 +31,10 @@ end local function tog_opacity() opacity_on = not opacity_on if opacity_on then + core.log("Opacity: on") system.set_window_opacity(current_opacity) else + core.log("Opacity: off") system.set_window_opacity(default_opacity) end end @@ -53,7 +56,14 @@ command.add(nil, { ["opacity:reset" ] = function() res_opacity() end, ["opacity:decrease"] = function() dec_opacity() end, ["opacity:increase"] = function() inc_opacity() end, - ["opacity:toggle mouse wheel use"] = function() use_mousewheel = not use_mousewheel end, + ["opacity:toggle mouse wheel use"] = function() + use_mousewheel = not use_mousewheel + if use_mousewheel then + core.log("Opacity (shift + mouse wheel): on") + else + core.log("Opacity (shift + mouse wheel): off") + end + end, }) keymap.add { diff --git a/plugins/open_ext.lua b/plugins/open_ext.lua index 8a98516..4c57d68 100644 --- a/plugins/open_ext.lua +++ b/plugins/open_ext.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 -- The general idea is to check if the file opened is valid utf-8 -- since lite-xl only supports UTF8 text, others can be safely assumed diff --git a/plugins/openfilelocation.lua b/plugins/openfilelocation.lua index 4b89815..603c7b6 100644 --- a/plugins/openfilelocation.lua +++ b/plugins/openfilelocation.lua @@ -1,18 +1,32 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" +local common = require "core.common" local command = require "core.command" local config = require "core.config" - -config.plugins.openfilelocation = {} +local platform_filemanager if PLATFORM == "Windows" then - config.plugins.openfilelocation.filemanager = "explorer" + platform_filemanager = "explorer" elseif PLATFORM == "Mac OS X" then - config.plugins.openfilelocation.filemanager = "open" + platform_filemanager = "open" else - config.plugins.openfilelocation.filemanager = "xdg-open" + platform_filemanager = "xdg-open" end +config.plugins.openfilelocation = common.merge({ + filemanager = platform_filemanager, + -- The config specification used by the settings gui + config_spec = { + name = "Open File Location", + { + label = "File Manager", + description = "Command of the file browser.", + path = "filemanager", + type = "string", + default = platform_filemanager + } + } +}, config.plugins.openfilelocation) command.add("core.docview", { ["open-file-location:open-file-location"] = function() diff --git a/plugins/openselected.lua b/plugins/openselected.lua index af00194..6333da9 100644 --- a/plugins/openselected.lua +++ b/plugins/openselected.lua @@ -1,19 +1,35 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" +local common = require "core.common" local config = require "core.config" +local contextmenu = require "plugins.contextmenu" -config.plugins.openselected = {} +local platform_filelauncher if PLATFORM == "Windows" then - config.plugins.openselected.filemanager = "start" + platform_filelauncher = "start" elseif PLATFORM == "Mac OS X" then - config.plugins.openselected.filemanager = "open" + platform_filelauncher = "open" else - config.plugins.openselected.filemanager = "xdg-open" + platform_filelauncher = "xdg-open" end +config.plugins.openselected = common.merge({ + filelauncher = platform_filelauncher, + -- The config specification used by the settings gui + config_spec = { + name = "Open Selected Text", + { + label = "File Launcher", + description = "Command used to open the selected path or link externally.", + path = "filelauncher", + type = "string", + default = platform_filelauncher + } + } +}, config.plugins.openselected) command.add("core.docview", { ["open-selected:open-selected"] = function() @@ -35,10 +51,16 @@ command.add("core.docview", { core.log("Opening %s...", text) - system.exec(config.plugins.openselected.filemanager .. " " .. text) + system.exec(config.plugins.openselected.filelauncher .. " " .. text) end, }) -keymap.add { ["ctrl+shift+o"] = "open-selected:open-selected" } +contextmenu:register("core.docview", { + contextmenu.DIVIDER, + { text = "Open Selection", command = "open-selected:open-selected" } +}) + + +keymap.add { ["ctrl+alt+o"] = "open-selected:open-selected" } diff --git a/plugins/pdfview.lua b/plugins/pdfview.lua index d5d749a..199584e 100644 --- a/plugins/pdfview.lua +++ b/plugins/pdfview.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" diff --git a/plugins/primary_selection.lua b/plugins/primary_selection.lua new file mode 100644 index 0000000..388caf0 --- /dev/null +++ b/plugins/primary_selection.lua @@ -0,0 +1,187 @@ +-- mod-version:3 +local core = require "core" +local Doc = require "core.doc" +local command = require "core.command" +local keymap = require "core.keymap" +local config = require "core.config" +local common = require "core.common" + +local function string_to_cmd(s) + local result = {} + for match in s:gmatch("%g+") do + table.insert(result, match) + end + return result +end + +config.plugins.primary_selection = common.merge({ + command_in = { "xclip", "-in", "-selection", "primary" }, -- Command to use to copy the selection + command_out = { "xclip", "-out", "-selection", "primary" }, -- Command to use to obtain the selection + set_cursor = true, -- Set cursor on middle mouse click + min_copy_time = 0.150, -- How much time to delay setting the selection; in seconds + config_spec = { + name = "Primary selection", + { + label = "Command copy", + description = "Command to use to copy the selection.", + path = "_command_in", + type = "string", + default = "xclip -in -selection primary", + on_apply = function(value) + config.plugins.primary_selection.command_in = string_to_cmd(value) + end, + }, + { + label = "Command paste", + description = "Command to use to obtain the selection.", + path = "_command_out", + type = "string", + default = "xclip -out -selection primary", + on_apply = function(value) + config.plugins.primary_selection.command_out = string_to_cmd(value) + end, + }, + { + label = "Set cursor", + description = "Set cursor on middle mouse click.", + path = "set_cursor", + type = "toggle", + default = true, + }, + { + label = "Copy timeout", + description = "How much time to delay setting the selection; in milliseconds.", + path = "min_copy_time_ms", + type = "number", + default = 150, + min = 0, + step = 50, + on_apply = function(value) + config.plugins.primary_selection.min_copy_time = value / 1000 + end + }, + } +}, config.plugins.primary_selection) + + +local last_selection_data +--[[ + = { + time = nil, + line1 = nil, + col1 = nil, + line2 = nil, + col2 = nil, + doc = nil, +} +]] + +local xclip_copy +local function delayed_copy() + while true do + local data = last_selection_data + if not data then return end + local current_time = system.get_time() + local diff_time = current_time - data.time + -- Check if enough time has passed since last selection change + if diff_time >= config.plugins.primary_selection.min_copy_time then + if xclip_copy then xclip_copy:terminate() end + if not config.plugins.primary_selection.command_in + or #config.plugins.primary_selection.command_in == 0 then + core.warn("No primary selection copy command set") + break + end + xclip_copy = process.start(config.plugins.primary_selection.command_in) + if not xclip_copy then + core.warn("Unable to start copy command") + break + end + local text = data.doc:get_text(data.line1, data.col1, data.line2, data.col2) + local nbytes = #text + local total_written = 0 + -- In some rare cases xclip isn't fast enough so we need to retry sending the data + local retry = 3 + repeat + local written, err = xclip_copy:write(text) + if written == 0 or not written then + if retry > 0 then + retry = retry - 1 + else + core.error("Error while setting primary selection. "..(err or "")) + break + end + else + retry = 3 + end + total_written = total_written + written + text = string.sub(text, written + 1) + until total_written >= nbytes + xclip_copy:close_stream(process.STREAM_STDIN) + -- We need to leave the process running as killing it would destroy the copied buffer + break + end + coroutine.yield() + end + last_selection_data = nil +end + + +local doc_set_selections = Doc.set_selections +function Doc:set_selections(...) + local result = doc_set_selections(self, ...) + local line1, col1, line2, col2 + line1, col1, line2, col2 = self:get_selection() + if line1 ~= line2 or col1 ~= col2 then + if not last_selection_data then + -- Start "timer" to confirm the selection only after `min_copy_time` has passed + core.add_thread(delayed_copy) + last_selection_data = { } + end + -- We could extract the text here, but it is a potentially heavy operation, + -- so we do it only when we're actually confirming the selection. + -- The drawback is that if the selection is overwritten/deleted, + -- it is either never sent, or is different than expected. + -- TODO: Confirm the selection on text change. + last_selection_data.time = system.get_time() + last_selection_data.line1 = line1 + last_selection_data.col1 = col1 + last_selection_data.line2 = line2 + last_selection_data.col2 = col2 + last_selection_data.doc = self + end + return result +end + + +command.add("core.docview", { + ["primary-selection:paste"] = function(x, y, clicks, ...) + if not config.plugins.primary_selection.command_out + or #config.plugins.primary_selection.command_out == 0 then + core.warn("No primary selection paste command set") + return + end + if x and config.plugins.primary_selection.set_cursor then + -- TODO: There must be a better way to do this + core.on_event("mousepressed", "left", x, y, clicks, ...) + core.on_event("mousereleased", "left", x, y, clicks, ...) + end + local xclip = process.start(config.plugins.primary_selection.command_out) + if not xclip then + core.warn("Unable to start paste command") + return + end + local text = {} + repeat + local buffer = xclip:read_stdout() + table.insert(text, buffer or "") + until not buffer + if #text > 0 then + core.active_view.doc:text_input(table.concat(text)) + end + end +}) + +keymap.add({ + ["1mclick"] = "primary-selection:paste" +}) + diff --git a/plugins/rainbowparen.lua b/plugins/rainbowparen.lua index 52e9d50..6ca4cb4 100644 --- a/plugins/rainbowparen.lua +++ b/plugins/rainbowparen.lua @@ -1,7 +1,23 @@ --- mod-version:2 -- lite-xl 2.0 -local tokenizer = require "core.tokenizer" +-- mod-version:3 +local core = require "core" local style = require "core.style" +local config = require "core.config" local common = require "core.common" +local command = require "core.command" +local tokenizer = require "core.tokenizer" +local Highlighter = require "core.doc.highlighter" + +config.plugins.rainbowparen = common.merge({ + enabled = true, + parens = 5 +}, config.plugins.rainbowparen) + +style.syntax.paren_unbalanced = style.syntax.paren_unbalanced or { common.color "#DC0408" } +style.syntax.paren1 = style.syntax.paren1 or { common.color "#FC6F71"} +style.syntax.paren2 = style.syntax.paren2 or { common.color "#fcb053"} +style.syntax.paren3 = style.syntax.paren3 or { common.color "#fcd476"} +style.syntax.paren4 = style.syntax.paren4 or { common.color "#52dab2"} +style.syntax.paren5 = style.syntax.paren5 or { common.color "#5a98cf"} local tokenize = tokenizer.tokenize local closers = { @@ -9,10 +25,15 @@ local closers = { ["["] = "]", ["{"] = "}" } + local function parenstyle(parenstack) - return "paren" .. ((#parenstack % 5) + 1) + return "paren" .. ((#parenstack % config.plugins.rainbowparen.parens) + 1) end + function tokenizer.tokenize(syntax, text, state) + if not config.plugins.rainbowparen.enabled then + return tokenize(syntax, text, state) + end state = state or {} local res, istate = tokenize(syntax, text, state.istate) local parenstack = state.parenstack or "" @@ -51,9 +72,31 @@ function tokenizer.tokenize(syntax, text, state) return newres, { parenstack = parenstack, istate = istate } end -style.syntax.paren_unbalanced = style.syntax.paren_unbalanced or { common.color "#DC0408" } -style.syntax.paren1 = style.syntax.paren1 or { common.color "#FC6F71"} -style.syntax.paren2 = style.syntax.paren2 or { common.color "#fcb053"} -style.syntax.paren3 = style.syntax.paren3 or { common.color "#fcd476"} -style.syntax.paren4 = style.syntax.paren4 or { common.color "#52dab2"} -style.syntax.paren5 = style.syntax.paren5 or { common.color "#5a98cf"} +local function toggle_rainbowparen(enabled) + config.plugins.rainbowparen.enabled = enabled + for _, doc in ipairs(core.docs) do + doc.highlighter = Highlighter(doc) + doc:reset_syntax() + end +end + +-- The config specification used by the settings gui +config.plugins.rainbowparen.config_spec = { + name = "Rainbow Parentheses", + { + label = "Enable", + description = "Activates rainbow parenthesis coloring by default.", + path = "enabled", + type = "toggle", + default = true, + on_apply = function(enabled) + toggle_rainbowparen(enabled) + end + } +} + +command.add(nil, { + ["rainbow-parentheses:toggle"] = function() + toggle_rainbowparen(not config.plugins.rainbowparen.enabled) + end +}) diff --git a/plugins/regexreplacepreview.lua b/plugins/regexreplacepreview.lua index 1c8b845..18d692b 100644 --- a/plugins/regexreplacepreview.lua +++ b/plugins/regexreplacepreview.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local keymap = require "core.keymap" local command = require "core.command" @@ -6,121 +6,125 @@ local command = require "core.command" -- Takes the following pattern: /pattern/replace/ -- Capture groupings can be replaced using \1 through \9 local function regex_replace_file(view, pattern, old_lines, raw, start_line, end_line) - local doc = view.doc - local start_pattern, end_pattern, end_replacement, start_replacement = 2, 2; - repeat - end_pattern = string.find(pattern, "/", end_pattern) - until end_pattern == nil or pattern[end_pattern-1] ~= "\\" - if end_pattern == nil then - end_pattern = #pattern + 1 - else - end_pattern = end_pattern - 1 - start_replacement = end_pattern+2; - end_replacement = end_pattern+2; - repeat - end_replacement = string.find(pattern, "/", end_replacement) - until end_replacement == nil or pattern[end_replacement-1] ~= "\\" - end - end_replacement = end_replacement and (end_replacement - 1) - - local re = start_pattern ~= end_pattern and regex.compile(pattern:sub(start_pattern, end_pattern)) - - local replacement = end_replacement and pattern:sub(start_replacement, end_replacement) - local replace_line = raw and function(line, new_text) - if line == #doc.lines then - doc:raw_remove(line, 1, line, #doc.lines[line], { idx = 1 }, 0) - else - doc:raw_remove(line, 1, line+1, 1, { idx = 1 }, 0) - end - doc:raw_insert(line, 1, new_text, { idx = 1 }, 0) - end or function(line, new_text) - if line == #doc.lines then - doc:remove(line, 1, line, #doc.lines[line]) - else - doc:remove(line, 1, line+1, 1) - end - doc:insert(line, 1, new_text) - end - - local line_scroll = nil - if re then - for i = (start_line or 1), (end_line or #doc.lines) do - local new_text, matches, rmatches - local old_text = old_lines[i] or doc.lines[i] - local old_length = #old_text - if replacement then - new_text, matches, rmatches = regex.gsub(re, old_text, replacement) - end - if matches and #matches > 0 then - old_lines[i] = old_text - replace_line(i, new_text) - if line_scroll == nil then - line_scroll = i - doc:set_selection(i, rmatches[1][1], i, rmatches[1][2]) - end - elseif old_lines[i] then - replace_line(i, old_lines[i]) - old_lines[i] = nil - end - if not replacement then - local s,e = regex.match(re, old_text) - if s then - line_scroll = i - doc:set_selection(i, s, i, e) - break - end - end + local doc = view.doc + local start_pattern, end_pattern, end_replacement, start_replacement = 2, 2; + repeat + end_pattern = string.find(pattern, "/", end_pattern) + until end_pattern == nil or pattern[end_pattern-1] ~= "\\" + if end_pattern == nil then + end_pattern = #pattern + 1 + else + end_pattern = end_pattern - 1 + start_replacement = end_pattern+2; + end_replacement = end_pattern+2; + repeat + end_replacement = string.find(pattern, "/", end_replacement) + until end_replacement == nil or pattern[end_replacement-1] ~= "\\" + end + end_replacement = end_replacement and (end_replacement - 1) + + local re = start_pattern ~= end_pattern + and regex.compile(pattern:sub(start_pattern, end_pattern)) + + local replacement = end_replacement and pattern:sub( + start_replacement, end_replacement + ) + local replace_line = raw and function(line, new_text) + if line == #doc.lines then + doc:raw_remove(line, 1, line, #doc.lines[line], { idx = 1 }, 0) + else + doc:raw_remove(line, 1, line+1, 1, { idx = 1 }, 0) + end + doc:raw_insert(line, 1, new_text, { idx = 1 }, 0) + end or function(line, new_text) + if line == #doc.lines then + doc:remove(line, 1, line, #doc.lines[line]) + else + doc:remove(line, 1, line+1, 1) + end + doc:insert(line, 1, new_text) + end + + local line_scroll = nil + if re then + for i = (start_line or 1), (end_line or #doc.lines) do + local new_text, matches, rmatches + local old_text = old_lines[i] or doc.lines[i] + local old_length = #old_text + if replacement then + new_text, matches, rmatches = regex.gsub(re, old_text, replacement) end - if line_scroll then - view:scroll_to_line(line_scroll, true) + if matches and #matches > 0 then + old_lines[i] = old_text + replace_line(i, new_text) + if line_scroll == nil then + line_scroll = i + doc:set_selection(i, rmatches[1][1], i, rmatches[1][2]) + end + elseif old_lines[i] then + replace_line(i, old_lines[i]) + old_lines[i] = nil end - end - if replacement == nil then - for k,v in pairs(old_lines) do - replace_line(k, v) + if not replacement then + local s,e = regex.match(re, old_text) + if s then + line_scroll = i + doc:set_selection(i, s, i, e) + break + end end - old_lines = {} - end - return old_lines, line_scroll ~= nil + end + if line_scroll then + view:scroll_to_line(line_scroll, true) + end + end + if replacement == nil then + for k,v in pairs(old_lines) do + replace_line(k, v) + end + old_lines = {} + end + return old_lines, line_scroll ~= nil end command.add("core.docview", { - ["regex-replace-preview:find-replace-regex"] = function() - core.command_view:set_text("/") - local old_lines = {} - local view = core.active_view - local doc = view.doc - local original_selection = { doc:get_selection(true) } - local selection = doc:has_selection() and { doc:get_selection(true) } or {} - core.command_view:enter( - "Regex Replace (enter pattern as /old/new/)", - function(pattern) - regex_replace_file(view, pattern, {}, false, selection[1], selection[3]) - end, - function(pattern) - local incremental, has_replacement = regex_replace_file(view, pattern, old_lines, true, selection[1], selection[3]) - if incremental then - old_lines = incremental - end - if not has_replacement then - doc:set_selection(unpack(original_selection)) + ["regex-replace-preview:find-replace-regex"] = function() + local old_lines = {} + local view = core.active_view + local doc = view.doc + local original_selection = { doc:get_selection(true) } + local selection = doc:has_selection() and { doc:get_selection(true) } or {} + core.command_view:enter("Regex Replace (enter pattern as /old/new/)", { + text = "/", + submit = function(pattern) + regex_replace_file(view, pattern, {}, false, selection[1], selection[3]) + end, + suggest = function(pattern) + local incremental, has_replacement = regex_replace_file( + view, pattern, old_lines, true, selection[1], selection[3] + ) + if incremental then + old_lines = incremental + end + if not has_replacement then + doc:set_selection(table.unpack(original_selection)) + end + end, + cancel = function(pattern) + for k,v in pairs(old_lines) do + if v then + if k == #doc.lines then + doc:raw_remove(k, 1, k, #doc.lines[k], { idx = 1 }, 0) + else + doc:raw_remove(k, 1, k+1, 1, { idx = 1 }, 0) end - end, - function(pattern) - for k,v in pairs(old_lines) do - if v then - if k == #doc.lines then - doc:raw_remove(k, 1, k, #doc.lines[k], { idx = 1 }, 0) - else - doc:raw_remove(k, 1, k+1, 1, { idx = 1 }, 0) - end - doc:raw_insert(k, 1, v, { idx = 1 }, 0) - end - end - doc:set_selection(unpack(original_selection)) - end - ) - end + doc:raw_insert(k, 1, v, { idx = 1 }, 0) + end + end + doc:set_selection(table.unpack(original_selection)) + end + }) + end }) keymap.add { ["ctrl+shift+r"] = "regex-replace-preview:find-replace-regex" } diff --git a/plugins/restoretabs.lua b/plugins/restoretabs.lua index 5bcd977..4c33304 100644 --- a/plugins/restoretabs.lua +++ b/plugins/restoretabs.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 -- Not perfect, because we can't actually figure out when something closes, but should be good enough, so long as we check the list of open views. -- Maybe find a better way to get at "Node"? local core = require "core" @@ -17,7 +17,7 @@ RootView.update = function(self) if not initialized_tab_system then local Node = getmetatable(self.root_node) local old_close = Node.close_view - + Node.close_view = function(self, root, view) if view.doc and view.doc.abs_filename then local closing_filename = view.doc.abs_filename @@ -40,7 +40,7 @@ RootView.update = function(self) end -command.add("core.docview", { +command.add(nil, { ["restore-tabs:restore-tab"] = function() if #tab_history > 0 then local file = tab_history[#tab_history] diff --git a/plugins/scalestatus.lua b/plugins/scalestatus.lua index 8f3ef68..a7623b1 100644 --- a/plugins/scalestatus.lua +++ b/plugins/scalestatus.lua @@ -1,34 +1,54 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 --[[ scalestatus.lua displays current scale (zoom) in status view version: 20200628_155804 originally by SwissalpS --]] -local scale = require "plugins.scale" - +local core = require "core" +local common = require "core.common" local config = require "core.config" +local scale = require "plugins.scale" local StatusView = require "core.statusview" -config.plugins.scalestatus = { format = '%.0f%%' } - -local get_items = StatusView.get_items -function StatusView:get_items() - - local left, right = get_items(self) - - local t = { - self.separator, - string.format(config.plugins.scalestatus.format, scale.get() * 100), +config.plugins.scalestatus = common.merge({ + enabled = true, + format = '%.0f%%', + -- The config specification used by the settings gui + config_spec = { + name = "Scale Status", + { + label = "Enabled", + description = "Show or hide the scale status 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:scale"):show() + else + core.status_view:get_item("status:scale"):hide() + end + end) + end + } } - - for _, item in ipairs(t) do - table.insert(right, item) - end - - return left, right - -end +}, config.plugins.scalestatus) + +core.status_view:add_item({ + name = "status:scale", + alignment = StatusView.Item.RIGHT, + get_item = function() + return {string.format( + config.plugins.scalestatus.format, + scale.get() * 100 + )} + end, + position = 1, + tooltip = "scale", + separator = core.status_view.separator2 +}) return true diff --git a/plugins/select_colorscheme.lua b/plugins/select_colorscheme.lua index 1e25bc4..6fa45d4 100644 --- a/plugins/select_colorscheme.lua +++ b/plugins/select_colorscheme.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local common = require "core.common" @@ -65,10 +65,10 @@ local function make_color_module_name(name) end function Settings:change_color(name) - if self:is_change_color(name) then - core.reload_module(make_color_module_name(name)) - self.color_scheme = name - end + if self:is_change_color(name) then + core.reload_module(make_color_module_name(name)) + self.color_scheme = name + end end function Settings:save_settings() @@ -121,10 +121,12 @@ local color_scheme_suggest = function(text) end command.add(nil, { - ["ui:color scheme"] = function() - core.command_view:enter("Select color scheme", color_scheme_submit, color_scheme_suggest) - end, - }) + ["ui:color scheme"] = function() + core.command_view:enter("Select color scheme", { + submit = color_scheme_submit, suggest = color_scheme_suggest + }) + end, +}) -- ---------------------------------------------------------------- Settings:init() diff --git a/plugins/selectionhighlight.lua b/plugins/selectionhighlight.lua index 19bc475..133dced 100644 --- a/plugins/selectionhighlight.lua +++ b/plugins/selectionhighlight.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local style = require "core.style" local DocView = require "core.docview" @@ -16,15 +16,15 @@ end local draw_line_body = DocView.draw_line_body -function DocView:draw_line_body(idx, x, y) - draw_line_body(self, idx, x, y) +function DocView:draw_line_body(line, x, y) + local line_height = draw_line_body(self, line, x, y) local line1, col1, line2, col2 = self.doc:get_selection(true) if line1 == line2 and col1 ~= col2 then local selection = self.doc:get_text(line1, col1, line2, col2) if not selection:match("^%s+$") then local lh = self:get_line_height() local selected_text = self.doc.lines[line1]:sub(col1, col2 - 1) - local current_line_text = self.doc.lines[idx] + local current_line_text = self.doc.lines[line] local last_col = 1 while true do local start_col, end_col = current_line_text:find( @@ -32,9 +32,9 @@ function DocView:draw_line_body(idx, x, y) ) if start_col == nil then break end -- don't draw box around the selection - if idx ~= line1 or start_col ~= col1 then - local x1 = x + self:get_col_x_offset(idx, start_col) - local x2 = x + self:get_col_x_offset(idx, end_col + 1) + if line ~= line1 or start_col ~= col1 then + local x1 = x + self:get_col_x_offset(line, start_col) + local x2 = x + self:get_col_x_offset(line, end_col + 1) local color = style.selectionhighlight or style.syntax.comment draw_box(x1, y, x2 - x1, lh, color) end @@ -42,5 +42,6 @@ function DocView:draw_line_body(idx, x, y) end end end + return line_height end diff --git a/plugins/settings.lua b/plugins/settings.lua new file mode 100644 index 0000000..62d1592 --- /dev/null +++ b/plugins/settings.lua @@ -0,0 +1,1858 @@ +-- mod-version:3 --priority:0 +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 style = require "core.style" + +-- check if widget is installed before proceeding +local widget_found, Widget = pcall(require, "widget") +if not widget_found then + core.error("Widget library not found: https://github.com/lite-xl/lite-xl-widgets") + return +end + +local Label = require "widget.label" +local Line = require "widget.line" +local NoteBook = require "widget.notebook" +local Button = require "widget.button" +local TextBox = require "widget.textbox" +local SelectBox = require "widget.selectbox" +local NumberBox = require "widget.numberbox" +local Toggle = require "widget.toggle" +local ListBox = require "widget.listbox" +local FoldingBook = require "widget.foldingbook" +local FontsList = require "widget.fontslist" +local ItemsList = require "widget.itemslist" +local KeybindingDialog = require "widget.keybinddialog" +local Fonts = require "widget.fonts" +local FilePicker = require "widget.filepicker" + +local settings = {} + +settings.core = {} +settings.plugins = {} +settings.sections = {} +settings.plugin_sections = {} +settings.config = {} +settings.default_keybindings = {} + +---Enumeration for the different types of settings. +---@type table<string, integer> +settings.type = { + STRING = 1, + NUMBER = 2, + TOGGLE = 3, + SELECTION = 4, + LIST_STRINGS = 5, + BUTTON = 6, + FONT = 7, + FILE = 8, + DIRECTORY = 9 +} + +---@alias settings.types +---| `settings.type.STRING` +---| `settings.type.NUMBER` +---| `settings.type.TOGGLE` +---| `settings.type.SELECTION` +---| `settings.type.LIST_STRINGS` +---| `settings.type.BUTTON` +---| `settings.type.FONT` +---| `settings.type.FILE` + +---Represents a setting to render on a settings pane. +---@class settings.option +---@field public label string +---@field public description string +---@field public path string +---@field public type settings.types | integer +---@field public default string | number | boolean | table<integer, string> | table<integer, integer> +---@field public min number +---@field public max number +---@field public step number +---@field public values table +---@field public fonts_list table<string, renderer.font> +---@field public font_error boolean +---@field public get_value nil | fun(value:any):any +---@field public set_value nil | fun(value:any):any +---@field public icon string +---@field public on_click nil | string | fun(button:string, x:integer, y:integer) +---@field public on_apply nil | fun(value:any) +---@field public exists boolean +---@field public filters table<integer,string> +settings.option = { + ---Title displayed to the user eg: "My Option" + label = "", + ---Description of the option eg: "Modifies the document indentation" + description = "", + ---Config path in the config table, eg: section.myoption, myoption, etc... + path = "", + ---Type of option that will be used to render an appropriate control + type = "", + ---Default value of the option + default = "", + ---Used for NUMBER to indiciate the minimum number allowed + min = 0, + ---Used for NUMBER to indiciate the maximum number allowed + max = 0, + ---Used for NUMBER to indiciate the increment/decrement amount + step = 0, + ---Used in a SELECTION to provide the list of valid options + values = {}, + ---Optionally used for FONT to store the generated font group. + fonts_list = {}, + ---Flag set to true when loading user defined fonts fail + font_error = false, + ---Optional function that is used to manipulate the current value on retrieval. + get_value = nil, + ---Optional function that is used to manipulate the saved value on save. + set_value = nil, + ---The icon set for a BUTTON + icon = "", + ---Command or function executed when a BUTTON is clicked + on_click = nil, + ---Optional function executed when the option value is applied. + on_apply = nil, + ---When FILE or DIRECTORY this flag tells the path should exist. + exists = false, + ---Lua patterns used on FILE or DIRECTORY to filter browser results and + ---also force the selection to match one of the filters. + filters = {} +} + +---Add a new settings section to the settings UI +---@param section string +---@param options settings.option[] +---@param plugin_name? string Optional name of plugin +---@param overwrite? boolean Overwrite previous section options +function settings.add(section, options, plugin_name, overwrite) + local category = "" + if plugin_name ~= nil then + category = "plugins" + else + category = "core" + end + + if overwrite and settings[category][section] then + settings[category][section] = {} + end + + if not settings[category][section] then + settings[category][section] = {} + if category ~= "plugins" then + table.insert(settings.sections, section) + else + table.insert(settings.plugin_sections, section) + end + end + + if plugin_name ~= nil then + if not settings[category][section][plugin_name] then + settings[category][section][plugin_name] = {} + end + for _, option in ipairs(options) do + table.insert(settings[category][section][plugin_name], option) + end + else + for _, option in ipairs(options) do + table.insert(settings[category][section], option) + end + end +end + +-------------------------------------------------------------------------------- +-- Add Core Settings +-------------------------------------------------------------------------------- + +settings.add("General", + { + { + label = "User Module", + description = "Open your init.lua for customizations.", + type = settings.type.BUTTON, + icon = "P", + on_click = "core:open-user-module" + }, + { + label = "Clear Fonts Cache", + description = "Delete current font cache and regenerate a fresh one.", + type = settings.type.BUTTON, + icon = "C", + on_click = function() + Fonts.clean_cache() + end + }, + { + label = "Maximum Project Files", + description = "The maximum amount of project files to register.", + path = "max_project_files", + type = settings.type.NUMBER, + default = 2000, + min = 1, + max = 100000, + on_apply = function(button, x, y) + if button == "left" then + core.rescan_project_directories() + end + end + }, + { + label = "File Size Limit", + description = "The maximum file size in megabytes allowed for editing.", + path = "file_size_limit", + type = settings.type.NUMBER, + default = 10, + min = 1, + max = 50 + }, + { + label = "Ignore Files", + description = "List of lua patterns matching files to be ignored by the editor.", + path = "ignore_files", + type = settings.type.LIST_STRINGS, + default = { "^%." }, + on_apply = function() + core.rescan_project_directories() + end + }, + { + label = "Maximum Clicks", + description = "The maximum amount of consecutive clicks that are registered by the editor.", + path = "max_clicks", + type = settings.type.NUMBER, + default = 3, + min = 1, + max = 10 + }, + } +) + +settings.add("Graphics", + { + { + label = "Frames Per Second", + description = "Lower value for low end machines and higher for a smoother experience.", + path = "fps", + type = settings.type.NUMBER, + default = 60, + min = 10, + max = 300 + }, + { + label = "Transitions", + description = "If disabled turns off all transitions but improves rendering performance.", + path = "transitions", + type = settings.type.TOGGLE, + default = true + }, + { + label = "Animation Rate", + description = "The amount of time it takes for a transition to finish.", + path = "animation_rate", + type = settings.type.NUMBER, + default = 1.0, + min = 0.5, + max = 3.0, + step = 0.1 + }, + { + label = "Animate Mouse Drag Scroll", + description = "Causes higher cpu usage but smoother scroll transition.", + path = "animate_drag_scroll", + type = settings.type.TOGGLE, + default = false + }, + { + label = "Disable Scrolling Transitions", + path = "disabled_transitions.scroll", + type = settings.type.TOGGLE, + default = false + }, + { + label = "Disable Command View Transitions", + path = "disabled_transitions.commandview", + type = settings.type.TOGGLE, + default = false + }, + { + label = "Disable Context Menu Transitions", + path = "disabled_transitions.contextmenu", + type = settings.type.TOGGLE, + default = false + }, + { + label = "Disable Log View Transitions", + path = "disabled_transitions.logview", + type = settings.type.TOGGLE, + default = false + }, + { + label = "Disable Nag Bar Transitions", + path = "disabled_transitions.nagbar", + type = settings.type.TOGGLE, + default = false + }, + { + label = "Disable Tab Transitions", + path = "disabled_transitions.tabs", + type = settings.type.TOGGLE, + default = false + }, + { + label = "Disable Tab Drag Transitions", + path = "disabled_transitions.tab_drag", + type = settings.type.TOGGLE, + default = false + }, + { + label = "Disable Status Bar Transitions", + path = "disabled_transitions.statusbar", + type = settings.type.TOGGLE, + default = false + }, + } +) + +settings.add("User Interface", + { + { + label = "Font", + description = "The font and fallbacks used on non code text.", + path = "font", + type = settings.type.FONT, + fonts_list = style, + default = { + fonts = { + { + name = "Fira Sans Regular", + path = DATADIR .. "/fonts/FiraSans-Regular.ttf" + } + }, + options = { + size = 15, + antialiasing = "subpixel", + hinting = "slight" + } + } + }, + { + label = "Borderless", + description = "Use built-in window decorations.", + path = "borderless", + type = settings.type.TOGGLE, + default = false, + on_apply = function() + core.configure_borderless_window() + end + }, + { + label = "Always Show Tabs", + description = "Shows tabs even if a single document is opened.", + path = "always_show_tabs", + type = settings.type.TOGGLE, + default = true + }, + { + label = "Maximum Tabs", + description = "The maximum amount of visible document tabs.", + path = "max_tabs", + type = settings.type.NUMBER, + default = 8, + min = 1, + max = 100 + }, + { + label = "Close Button on Tabs", + description = "Display the close button on tabs.", + path = "tab_close_button", + type = settings.type.TOGGLE, + default = true + }, + { + label = "Mouse wheel scroll rate", + description = "The amount to scroll when using the mouse wheel.", + path = "mouse_wheel_scroll", + type = settings.type.NUMBER, + default = 50, + min = 10, + max = 200, + get_value = function(value) + return value / SCALE + end, + set_value = function(value) + return value * SCALE + end + }, + { + label = "Disable Cursor Blinking", + description = "Disables cursor blinking on text input elements.", + path = "disable_blink", + type = settings.type.TOGGLE, + default = false + }, + { + label = "Cursor Blinking Period", + description = "Interval in seconds in which the cursor blinks.", + path = "blink_period", + type = settings.type.NUMBER, + default = 0.8, + min = 0.3, + max = 2.0, + step = 0.1 + } + } +) + +settings.add("Editor", + { + { + label = "Code Font", + description = "The font and fallbacks used on the code editor.", + path = "code_font", + type = settings.type.FONT, + fonts_list = style, + default = { + fonts = { + { + name = "JetBrains Mono Regular", + path = DATADIR .. "/fonts/JetBrainsMono-Regular.ttf" + } + }, + options = { + size = 15, + antialiasing = "subpixel", + hinting = "slight" + } + } + }, + { + label = "Indentation Type", + description = "The character inserted when pressing the tab key.", + path = "tab_type", + type = settings.type.SELECTION, + default = "soft", + values = { + {"Space", "soft"}, + {"Tab", "hard"} + } + }, + { + label = "Indentation Size", + description = "Amount of spaces shown per indentation.", + path = "indent_size", + type = settings.type.NUMBER, + default = 2, + min = 1, + max = 10 + }, + { + label = "Line Limit", + description = "Amount of characters at which the line breaking column will be drawn.", + path = "line_limit", + type = settings.type.NUMBER, + default = 80, + min = 1 + }, + { + label = "Line Height", + description = "The amount of spacing between lines.", + path = "line_height", + type = settings.type.NUMBER, + default = 1.2, + min = 1.0, + max = 3.0, + step = 0.1 + }, + { + label = "Highlight Line", + description = "Highlight the current line.", + path = "highlight_current_line", + type = settings.type.SELECTION, + default = true, + values = { + {"Yes", true}, + {"No", false}, + {"No Selection", "no_selection"} + }, + set_value = function(value) + if type(value) == "nil" then return false end + return value + end + }, + { + label = "Maximum Undo History", + description = "The amount of undo elements to keep.", + path = "max_undos", + type = settings.type.NUMBER, + default = 10000, + min = 100, + max = 100000 + }, + { + label = "Undo Merge Timeout", + description = "Time in seconds before applying an undo action.", + path = "undo_merge_timeout", + type = settings.type.NUMBER, + default = 0.3, + min = 0.1, + max = 1.0, + step = 0.1 + }, + { + label = "Symbol Pattern", + description = "A lua pattern used to match symbols in the document.", + path = "symbol_pattern", + type = settings.type.STRING, + default = "[%a_][%w_]*" + }, + { + label = "Non Word Characters", + description = "A string of characters that do not belong to a word.", + path = "non_word_chars", + type = settings.type.STRING, + default = " \\t\\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-", + get_value = function(value) + return value:gsub("\n", "\\n"):gsub("\t", "\\t") + end, + set_value = function(value) + return value:gsub("\\n", "\n"):gsub("\\t", "\t") + end + }, + { + label = "Scroll Past the End", + description = "Allow scrolling beyond the document ending.", + path = "scroll_past_end", + type = settings.type.TOGGLE, + default = true + } + } +) + +settings.add("Development", + { + { + label = "Core Log", + description = "Open the list of logged messages.", + type = settings.type.BUTTON, + icon = "f", + on_click = "core:open-log" + }, + { + label = "Log Items", + description = "The maximum amount of entries to keep on the log UI.", + path = "max_log_items", + type = settings.type.NUMBER, + default = 80, + min = 50, + max = 2000 + }, + { + label = "Skip Plugins Version", + description = "Do not verify the plugins required versions at startup.", + path = "skip_plugins_version", + type = settings.type.TOGGLE, + default = false + } + } +) + +settings.add("Status Bar", + { + { + label = "Enabled", + description = "Toggle the default visibility of the status bar.", + path = "statusbar.enabled", + type = settings.type.TOGGLE, + default = true, + on_apply = function(enabled) + if enabled then + core.status_view:show() + else + core.status_view:hide() + end + end + }, + { + label = "Show Notifications", + description = "Toggle the visibility of status messages.", + path = "statusbar.messages", + type = settings.type.TOGGLE, + default = true, + on_apply = function(enabled) + core.status_view:display_messages(enabled) + end + }, + { + label = "Messages Timeout", + description = "The amount in seconds before a notification dissapears.", + path = "message_timeout", + type = settings.type.NUMBER, + default = 5, + min = 1, + max = 30 + } + } +) + +---Retrieve from given config the associated value using the given path. +---@param conf table +---@param path string +---@param default any +---@return any | nil +local function get_config_value(conf, path, default) + local sections = {}; + for match in (path.."."):gmatch("(.-)%.") do + table.insert(sections, match); + end + + local element = conf + for _, section in ipairs(sections) do + if type(element[section]) ~= "nil" then + element = element[section] + else + return default + end + end + + if type(element) == "nil" then + return default + end + + return element +end + +---Loops the given config table using the given path and store the value. +---@param conf table +---@param path string +---@param value any +local function set_config_value(conf, path, value) + local sections = {}; + for match in (path.."."):gmatch("(.-)%.") do + table.insert(sections, match); + end + + local sections_count = #sections + + if sections_count == 1 then + conf[sections[1]] = value + return + elseif type(conf[sections[1]]) ~= "table" then + conf[sections[1]] = {} + end + + local element = conf + for idx, section in ipairs(sections) do + if type(element[section]) ~= "table" then + element[section] = {} + element = element[section] + else + element = element[section] + end + if idx + 1 == sections_count then break end + end + + element[sections[sections_count]] = value +end + +---Get a list of system and user installed plugins. +---@return table<integer, string> +local function get_installed_plugins() + local files, ordered = {}, {} + + for _, root_dir in ipairs {DATADIR, USERDIR} do + local plugin_dir = root_dir .. "/plugins" + for _, filename in ipairs(system.list_dir(plugin_dir) or {}) do + local valid = false + local file_info = system.get_file_info(plugin_dir .. "/" .. filename) + if file_info then + if + file_info.type == "file" + and + filename:match("%.lua$") + and + not filename:match("^language_") + then + valid = true + filename = filename:gsub("%.lua$", "") + elseif file_info.type == "dir" then + if system.get_file_info(plugin_dir .. "/" .. filename .. "/init.lua") then + valid = true + end + end + end + if valid then + if not files[filename] then table.insert(ordered, filename) end + files[filename] = true + end + end + end + + table.sort(ordered) + + return ordered +end + +---Get a list of system and user installed colors. +---@return table<integer, table> +local function get_installed_colors() + local files, ordered = {}, {} + + for _, root_dir in ipairs {DATADIR, USERDIR} do + local dir = root_dir .. "/colors" + for _, filename in ipairs(system.list_dir(dir) or {}) do + local file_info = system.get_file_info(dir .. "/" .. filename) + if + file_info and file_info.type == "file" + and + filename:match("%.lua$") + then + -- read colors + local contents = io.open(dir .. "/" .. filename):read("*a") + local colors = {} + for r, g, b in contents:gmatch("#(%x%x)(%x%x)(%x%x)") do + r = tonumber(r, 16) + g = tonumber(g, 16) + b = tonumber(b, 16) + table.insert(colors, { r, g, b, 0xff }) + end + -- sort colors from darker to lighter + table.sort(colors, function(a, b) + return a[1] + a[2] + a[3] < b[1] + b[2] + b[3] + end) + -- remove duplicate colors + local b = {} + for i = #colors, 1, -1 do + local a = colors[i] + if a[1] == b[1] and a[2] == b[2] and a[3] == b[3] then + table.remove(colors, i) + else + b = colors[i] + end + end + -- insert color to ordered table if not duplicate + filename = filename:gsub("%.lua$", "") + if not files[filename] then + table.insert(ordered, {name = filename, colors = colors}) + end + files[filename] = true + end + end + end + + table.sort(ordered, function(a, b) return a.name < b.name end) + + return ordered +end + +---Capitalize first letter of every word. +---Taken from core.command. +---@param words string +---@return string +local function capitalize_first(words) + return words:sub(1, 1):upper() .. words:sub(2) +end + +---Similar to command prettify_name but also takes care of underscores. +---@param name string +---@return string +local function prettify_name(name) + name = name:gsub("[%-_]", " "):gsub("%S+", capitalize_first) + return name +end + +---Load config options from the USERDIR user_settings.lua and store them on +---settings.config for later usage. +local function load_settings() + local ok, t = pcall(dofile, USERDIR .. "/user_settings.lua") + settings.config = ok and t.config or {} +end + +---Save current config options into the USERDIR user_settings.lua +local function save_settings() + local fp = io.open(USERDIR .. "/user_settings.lua", "w") + if fp then + local output = "{\n [\"config\"] = " + .. common.serialize( + settings.config, + { pretty = true, escape = true, sort = true, initial_indent = 1 } + ):gsub("^%s+", "") + .. "\n}\n" + fp:write("return ", output) + fp:close() + end +end + +---Apply a keybinding and optionally save it. +---@param cmd string +---@param bindings table<integer, string> +---@param skip_save? boolean +---@return table | nil +local function apply_keybinding(cmd, bindings, skip_save) + local row_value = nil + local changed = false + + local original_bindings = { keymap.get_binding(cmd) } + for _, binding in ipairs(original_bindings) do + keymap.unbind(binding, cmd) + end + + if #bindings > 0 then + if + not skip_save + and + settings.config.custom_keybindings + and + settings.config.custom_keybindings[cmd] + then + settings.config.custom_keybindings[cmd] = {} + end + local shortcuts = "" + for _, binding in ipairs(bindings) do + if not binding:match("%+$") and binding ~= "" and binding ~= "none" then + keymap.add({[binding] = cmd}) + shortcuts = shortcuts .. binding .. "\n" + if not skip_save then + if not settings.config.custom_keybindings then + settings.config.custom_keybindings = {} + settings.config.custom_keybindings[cmd] = {} + elseif not settings.config.custom_keybindings[cmd] then + settings.config.custom_keybindings[cmd] = {} + end + table.insert(settings.config.custom_keybindings[cmd], binding) + changed = true + end + end + end + if shortcuts ~= "" then + local bindings_list = shortcuts:gsub("\n$", "") + row_value = { + style.text, cmd, ListBox.COLEND, style.dim, bindings_list + } + end + elseif + not skip_save + and + settings.config.custom_keybindings + and + settings.config.custom_keybindings[cmd] + then + settings.config.custom_keybindings[cmd] = nil + changed = true + end + + if changed then + save_settings() + end + + if not row_value then + row_value = { + style.text, cmd, ListBox.COLEND, style.dim, "none" + } + end + + return row_value +end + +---Load the saved fonts into the config path or fonts_list table. +---@param option settings.option +---@param path string +---@param saved_value any +local function merge_font_settings(option, path, saved_value) + local font_options = saved_value.options or { + size = 15, + antialiasing = "supixel", + hinting = "slight" + } + font_options.size = font_options.size or 15 + font_options.antialiasing = font_options.antialiasing or "subpixel" + font_options.hinting = font_options.hinting or "slight" + + local fonts = {} + local font_loaded = true + for _, font in ipairs(saved_value.fonts) do + local font_data = nil + font_loaded = core.try(function() + font_data = renderer.font.load( + font.path, font_options.size * SCALE, font_options + ) + end) + if font_loaded then + table.insert(fonts, font_data) + else + option.font_error = true + core.error("Settings: could not load %s\n'%s - %s'", path, font.name, font.path) + break + end + end + + if font_loaded then + if option.fonts_list then + set_config_value(option.fonts_list, option.path, renderer.font.group(fonts)) + else + set_config_value(config, path, renderer.font.group(fonts)) + end + end +end + +---Merge previously saved settings without destroying the config table. +local function merge_settings() + if type(settings.config) ~= "table" then return end + + -- merge core settings + for _, section in ipairs(settings.sections) do + local options = settings.core[section] + for _, option in ipairs(options) do + if type(option.path) == "string" then + local saved_value = get_config_value(settings.config, option.path) + if type(saved_value) ~= "nil" then + if option.type == settings.type.FONT or option.type == "font" then + merge_font_settings(option, option.path, saved_value) + else + set_config_value(config, option.path, saved_value) + end + if option.on_apply then + option.on_apply(saved_value) + end + end + end + end + end + + -- merge plugin settings + table.sort(settings.plugin_sections) + for _, section in ipairs(settings.plugin_sections) do + local plugins = settings.plugins[section] + for plugin_name, options in pairs(plugins) do + for _, option in pairs(options) do + if type(option.path) == "string" then + local path = "plugins." .. plugin_name .. "." .. option.path + local saved_value = get_config_value(settings.config, path) + if type(saved_value) ~= "nil" then + if option.type == settings.type.FONT or option.type == "font" then + merge_font_settings(option, path, saved_value) + else + set_config_value(config, path, saved_value) + end + if option.on_apply then + option.on_apply(saved_value) + end + end + end + end + end + end + + -- apply custom keybindings + if settings.config.custom_keybindings then + for cmd, bindings in pairs(settings.config.custom_keybindings) do + apply_keybinding(cmd, bindings, true) + end + end +end + +---Scan all plugins to check if they define a config_spec and load it. +local function scan_plugins_spec() + for plugin, conf in pairs(config.plugins) do + if type(conf) == "table" and conf.config_spec then + settings.add( + conf.config_spec.name, + conf.config_spec, + plugin + ) + end + end +end + +---Called at core first run to store the default keybindings. +local function store_default_keybindings() + for name, _ in pairs(command.map) do + local keys = { keymap.get_binding(name) } + if #keys > 0 then + settings.default_keybindings[name] = keys + end + end +end + +---@class settings.ui : widget +---@field private notebook widget.notebook +---@field private core widget +---@field private colors widget +---@field private plugins widget +---@field private keybinds widget +---@field private about widget +---@field private core_sections widget.foldingbook +---@field private plugin_sections widget.foldingbook +local Settings = Widget:extend() + +---Constructor +function Settings:new() + Settings.super.new(self, nil, false) + + self.name = "Settings" + self.defer_draw = false + self.border.width = 0 + self.draggable = false + self.scrollable = false + + ---@type widget.notebook + self.notebook = NoteBook(self) + self.notebook.size.x = 250 + self.notebook.size.y = 300 + self.notebook.border.width = 0 + + self.core = self.notebook:add_pane("core", "Core") + self.colors = self.notebook:add_pane("colors", "Colors") + self.plugins = self.notebook:add_pane("plugins", "Plugins") + self.keybinds = self.notebook:add_pane("keybindings", "Keybindings") + self.about = self.notebook:add_pane("about", "About") + + self.notebook:set_pane_icon("core", "P") + self.notebook:set_pane_icon("colors", "W") + self.notebook:set_pane_icon("plugins", "B") + self.notebook:set_pane_icon("keybindings", "M") + self.notebook:set_pane_icon("about", "i") + + self.core_sections = FoldingBook(self.core) + self.core_sections.border.width = 0 + self.core_sections.scrollable = false + + self.plugin_sections = FoldingBook(self.plugins) + self.plugin_sections.border.width = 0 + self.plugin_sections.scrollable = false + + self:load_core_settings() + self:load_color_settings() + self:load_plugin_settings() + self:load_keymap_settings() + + self:setup_about() +end + +---Helper function to add control for both core and plugin settings. +---@oaram pane widget +---@param option settings.option +---@param plugin_name? string | nil +local function add_control(pane, option, plugin_name) + local found = false + local path = type(plugin_name) ~= "nil" and + "plugins." .. plugin_name .. "." .. option.path or option.path + local option_value = nil + if type(path) ~= "nil" then + option_value = get_config_value(config, path, option.default) + end + + if option.get_value then + option_value = option.get_value(option_value) + end + + ---@type widget + local widget = nil + + if type(option.type) == "string" then + option.type = settings.type[option.type:upper()] + end + + if option.type == settings.type.NUMBER then + ---@type widget.label + Label(pane, option.label .. ":") + ---@type widget.numberbox + local number = NumberBox(pane, option_value, option.min, option.max, option.step) + widget = number + found = true + + elseif option.type == settings.type.TOGGLE then + ---@type widget.toggle + local toggle = Toggle(pane, option.label, option_value) + widget = toggle + found = true + + elseif option.type == settings.type.STRING then + ---@type widget.label + Label(pane, option.label .. ":") + ---@type widget.textbox + local string = TextBox(pane, option_value or "") + widget = string + found = true + + elseif option.type == settings.type.SELECTION then + ---@type widget.label + Label(pane, option.label .. ":") + ---@type widget.selectbox + local select = SelectBox(pane) + for _, data in pairs(option.values) do + select:add_option(data[1], data[2]) + end + for idx, _ in ipairs(select.list.rows) do + if select.list:get_row_data(idx) == option_value then + select:set_selected(idx-1) + break + end + end + widget = select + found = true + + elseif option.type == settings.type.BUTTON then + ---@type widget.button + local button = Button(pane, option.label) + if option.icon then + button:set_icon(option.icon) + end + if option.on_click then + local command_type = type(option.on_click) + if command_type == "string" then + function button:on_click() + command.perform(option.on_click) + end + elseif command_type == "function" then + button.on_click = option.on_click + end + end + widget = button + found = true + + elseif option.type == settings.type.LIST_STRINGS then + ---@type widget.label + Label(pane, option.label .. ":") + ---@type widget.itemslist + local list = ItemsList(pane) + if type(option_value) == "table" then + for _, value in ipairs(option_value) do + list:add_item(value) + end + end + widget = list + found = true + + elseif option.type == settings.type.FONT then + --get fonts without conversion to renderer.font + if type(path) ~= "nil" then + if not option.font_error then + option_value = get_config_value(settings.config, path, option.default) + else + --fallback to default fonts if error loading user defined ones + option_value = option.default + end + end + ---@type widget.label + Label(pane, option.label .. ":") + ---@type widget.fontslist + local fonts = FontsList(pane) + if type(option_value) == "table" then + for _, font in ipairs(option_value.fonts) do + fonts:add_font(font) + end + + local font_options = option_value.options or { + size = 15, + antialiasing = "supixel", + hinting = "slight" + } + font_options.size = font_options.size or 15 + font_options.antialiasing = font_options.antialiasing or "subpixel" + font_options.hinting = font_options.hinting or "slight" + fonts:set_options(font_options) + end + widget = fonts + found = true + + elseif option.type == settings.type.FILE then + ---@type widget.label + Label(pane, option.label .. ":") + ---@type widget.filepicker + local file = FilePicker(pane, option_value or "") + if option.exists then + file:set_mode(FilePicker.mode.FILE_EXISTS) + else + file:set_mode(FilePicker.mode.FILE) + end + file.filters = option.filters or {} + widget = file + found = true + + elseif option.type == settings.type.DIRECTORY then + ---@type widget.label + Label(pane, option.label .. ":") + ---@type widget.filepicker + local file = FilePicker(pane, option_value or "") + if option.exists then + file:set_mode(FilePicker.mode.DIRECTORY_EXISTS) + else + file:set_mode(FilePicker.mode.DIRECTORY) + end + file.filters = option.filters or {} + widget = file + found = true + end + + if widget and type(path) ~= "nil" then + function widget:on_change(value) + if self:is(SelectBox) then + value = self:get_selected_data() + elseif self:is(ItemsList) then + value = self:get_items() + elseif self:is(FontsList) then + value = { + fonts = self:get_fonts(), + options = self:get_options() + } + end + + if option.set_value then + value = option.set_value(value) + end + + if self:is(FontsList) then + local fonts = {} + for _, font in ipairs(value.fonts) do + table.insert(fonts, renderer.font.load( + font.path, value.options.size * SCALE, value.options + )) + end + if option.fonts_list then + set_config_value(option.fonts_list, path, renderer.font.group(fonts)) + else + set_config_value(config, path, renderer.font.group(fonts)) + end + else + set_config_value(config, path, value) + end + + set_config_value(settings.config, path, value) + save_settings() + if option.on_apply then + option.on_apply(value) + end + end + end + + if (option.description or option.default) and found then + local text = option.description or "" + local default = "" + local default_type = type(option.default) + if default_type ~= "table" and default_type ~= "nil" then + if text ~= "" then + text = text .. " " + end + default = string.format("(default: %s)", option.default) + end + ---@type widget.label + local description = Label(pane, text .. default) + description.desc = true + end +end + +---Generate all the widgets for core settings. +function Settings:load_core_settings() + for _, section in ipairs(settings.sections) do + local options = settings.core[section] + + ---@type widget|widget.foldingbook.pane + local pane = self.core_sections:get_pane(section) + if not pane then + pane = self.core_sections:add_pane(section, section) + else + pane = pane.container + end + + for _, opt in ipairs(options) do + ---@type settings.option + local option = opt + add_control(pane, option) + end + end +end + +---Function in charge of rendering the colors column of the color pane. +---@param self widget.listbox +---@oaram row integer +---@param x integer +---@param y integer +---@param font renderer.font +---@param color renderer.color +---@param only_calc boolean +---@return number width +---@return number height +local function on_color_draw(self, row, x, y, font, color, only_calc) + local w = self:get_width() - (x - self.position.x) - style.padding.x + local h = font:get_height() + + if not only_calc then + local row_data = self:get_row_data(row) + local width = w/#row_data.colors + + for i = 1, #row_data.colors do + renderer.draw_rect(x + ((i - 1) * width), y, width, h, row_data.colors[i]) + end + end + + return w, h +end + +---Generate the list of all available colors with preview +function Settings:load_color_settings() + self.colors.scrollable = false + + local colors = get_installed_colors() + + ---@type widget.listbox + local listbox = ListBox(self.colors) + + listbox.border.width = 0 + listbox:enable_expand(true) + + listbox:add_column("Theme") + listbox:add_column("Colors") + + for idx, details in ipairs(colors) do + local name = details.name + if settings.config.theme and settings.config.theme == name then + listbox:set_selected(idx) + end + listbox:add_row({ + style.text, name, ListBox.COLEND, on_color_draw + }, {name = name, colors = details.colors}) + end + + function listbox:on_row_click(idx, data) + core.reload_module("colors." .. data.name) + settings.config.theme = data.name + save_settings() + end +end + +---Unload a plugin settings from plugins section. +---@param plugin string +function Settings:disable_plugin(plugin) + for _, section in ipairs(settings.plugin_sections) do + local plugins = settings.plugins[section] + + for plugin_name, options in pairs(plugins) do + if plugin_name == plugin then + self.plugin_sections:delete_pane(section) + end + end + end + + if + type(settings.config.enabled_plugins) == "table" + and + settings.config.enabled_plugins[plugin] + then + settings.config.enabled_plugins[plugin] = nil + end + if type(settings.config.disabled_plugins) ~= "table" then + settings.config.disabled_plugins = {} + end + + settings.config.disabled_plugins[plugin] = true + save_settings() +end + +---Load plugin and append its settings to the plugins section. +---@param plugin string +function Settings:enable_plugin(plugin) + local loaded = false + local config_type = type(config.plugins[plugin]) + if config_type == "boolean" or config_type == "nil" then + config.plugins[plugin] = {} + loaded = true + end + + require("plugins." .. plugin) + + if config.plugins[plugin] and config.plugins[plugin].config_spec then + local conf = config.plugins[plugin].config_spec + settings.add(conf.name, conf, plugin, true) + end + + for _, section in ipairs(settings.plugin_sections) do + local plugins = settings.plugins[section] + + for plugin_name, options in pairs(plugins) do + if plugin_name == plugin then + ---@type widget + local pane = self.plugin_sections:get_pane(section) + if not pane then + pane = self.plugin_sections:add_pane(section, section) + else + pane = pane.container + end + + for _, opt in ipairs(options) do + ---@type settings.option + local option = opt + add_control(pane, option, plugin_name) + end + end + end + end + + if + type(settings.config.disabled_plugins) == "table" + and + settings.config.disabled_plugins[plugin] + then + settings.config.disabled_plugins[plugin] = nil + end + if type(settings.config.enabled_plugins) ~= "table" then + settings.config.enabled_plugins = {} + end + + settings.config.enabled_plugins[plugin] = true + save_settings() + + if loaded then + core.log("Loaded '%s' plugin", plugin) + end +end + +---Generate all the widgets for plugin settings. +function Settings:load_plugin_settings() + ---@type widget + local pane = self.plugin_sections:get_pane("enable_disable") + if not pane then + pane = self.plugin_sections:add_pane("enable_disable", "Installed") + else + pane = pane.container + end + + -- requires earlier access to startup process + Label( + pane, + "Notice: disabling plugins will not take effect until next restart" + ) + + Line(pane, 2, 10) + + local plugins = get_installed_plugins() + for _, plugin in ipairs(plugins) do + if plugin ~= "settings" then + local enabled = false + + if + ( + type(config.plugins[plugin]) ~= "nil" + and + config.plugins[plugin] ~= false + ) + or + ( + settings.config.enabled_plugins + and + settings.config.enabled_plugins[plugin] + ) + then + enabled = true + end + + local this = self + + ---@type widget.toggle + local toggle = Toggle(pane, prettify_name(plugin), enabled) + function toggle:on_change(value) + if value then + this:enable_plugin(plugin) + else + this:disable_plugin(plugin) + end + end + end + end + + table.sort(settings.plugin_sections) + + for _, section in ipairs(settings.plugin_sections) do + local plugins = settings.plugins[section] + + for plugin_name, options in pairs(plugins) do + ---@type widget + local pane = self.plugin_sections:get_pane(section) + if not pane then + pane = self.plugin_sections:add_pane(section, section) + else + pane = pane.container + end + + for _, opt in ipairs(options) do + ---@type settings.option + local option = opt + add_control(pane, option, plugin_name) + end + end + end +end + +---@type widget.keybinddialog +local keymap_dialog = KeybindingDialog() + +function keymap_dialog:on_save(bindings) + local row_value = apply_keybinding(self.command, bindings) + if row_value then + self.listbox:set_row(self.row_id, row_value) + end +end + +function keymap_dialog:on_reset() + local default_keys = settings.default_keybindings[self.command] + local current_keys = { keymap.get_binding(self.command) } + + for _, binding in ipairs(current_keys) do + keymap.unbind(binding, self.command) + end + + if default_keys and #default_keys > 0 then + local cmd = self.command + if not settings.config.custom_keybindings then + settings.config.custom_keybindings = {} + settings.config.custom_keybindings[cmd] = {} + elseif not settings.config.custom_keybindings[cmd] then + settings.config.custom_keybindings[cmd] = {} + end + local shortcuts = "" + for _, binding in ipairs(default_keys) do + keymap.add({[binding] = cmd}) + shortcuts = shortcuts .. binding .. "\n" + table.insert(settings.config.custom_keybindings[cmd], binding) + end + local bindings_list = shortcuts:gsub("\n$", "") + self.listbox:set_row(self.row_id, { + style.text, cmd, ListBox.COLEND, style.dim, bindings_list + }) + else + self.listbox:set_row(self.row_id, { + style.text, self.command, ListBox.COLEND, style.dim, "none" + }) + end + if + settings.config.custom_keybindings + and + settings.config.custom_keybindings[self.command] + then + settings.config.custom_keybindings[self.command] = nil + save_settings() + end +end + +---Generate the list of all available commands and allow editing their keymaps. +function Settings:load_keymap_settings() + self.keybinds.scrollable = false + + local ordered = {} + for name, _ in pairs(command.map) do + table.insert(ordered, name) + end + table.sort(ordered) + + ---@type widget.listbox + local listbox = ListBox(self.keybinds) + + listbox.border.width = 0 + listbox:enable_expand(true) + + listbox:add_column("Command") + listbox:add_column("Bindings") + + for _, name in ipairs(ordered) do + local keys = { keymap.get_binding(name) } + local binding = "" + if #keys == 1 then + binding = keys[1] + elseif #keys > 1 then + binding = keys[1] + for idx, key in ipairs(keys) do + if idx ~= 1 then + binding = binding .. "\n" .. key + end + end + elseif #keys < 1 then + binding = "none" + end + listbox:add_row({ + style.text, name, ListBox.COLEND, style.dim, binding + }, name) + end + + function listbox:on_row_click(idx, data) + if not keymap_dialog:is_visible() then + local bindings = { keymap.get_binding(data) } + keymap_dialog:set_bindings(bindings) + keymap_dialog.row_id = idx + keymap_dialog.command = data + keymap_dialog.listbox = self + keymap_dialog:show() + end + end +end + +function Settings:setup_about() + ---@type widget.label + local title = Label(self.about, "Lite XL") + title.font = "big_font" + ---@type widget.label + local version = Label(self.about, "version " .. VERSION) + ---@type widget.label + local description = Label( + self.about, + "A lightweight text editor written in Lua, adapted from lite." + ) + + local function open_link(link) + local platform_filelauncher + if PLATFORM == "Windows" then + platform_filelauncher = "start" + elseif PLATFORM == "Mac OS X" then + platform_filelauncher = "open" + else + platform_filelauncher = "xdg-open" + end + system.exec(platform_filelauncher .. " " .. link) + end + + ---@type widget.button + local button = Button(self.about, "Visit Website") + button:set_tooltip("Open https://lite-xl.com/") + function button:on_click() open_link("https://lite-xl.com/") end + + ---@type widget.listbox + local contributors = ListBox(self.about) + contributors.scrollable = true + contributors:add_column("Contributors") + contributors:add_column("") + contributors:add_column("Website") + function contributors:on_row_click(_, data) open_link(data) end + +local contributors_list = { + { "Rxi", "Lite Founder", "https://github.com/rxi" }, + { "Francesco Abbate", "Lite XL Founder", "https://github.com/franko" }, + { "Adam Harrison", "Core", "https://github.com/adamharrison" }, + { "Andrea Zanellato", "CI, Website", "https://github.com/redtide" }, + { "Björn Buckwalter", "MacOS Support", "https://github.com/bjornbm" }, + { "boppyt", "Contributor", "https://github.com/boppyt" }, + { "Cukmekerb", "Contributor", "https://github.com/vincens2005" }, + { "Daniel Rocha", "Contributor", "https://github.com/dannRocha" }, + { "daubaris", "Contributor", "https://github.com/daubaris" }, + { "Dheisom Gomes", "Contributor", "https://github.com/dheisom" }, + { "Evgeny Petrovskiy", "Contributor", "https://github.com/eugenpt" }, + { "Ferdinand Prantl", "Contributor", "https://github.com/prantlf" }, + { "Jan", "Build System", "https://github.com/Jan200101" }, + { "Janis-Leuenberger", "MacOS Support", "https://github.com/Janis-Leuenberger" }, + { "Jefferson", "Contributor", "https://github.com/jgmdev" }, + { "Jipok", "Contributor", "https://github.com/Jipok" }, + { "Joshua Minor", "Contributor", "https://github.com/jminor" }, + { "George Linkovsky", "Contributor", "https://github.com/Timofffee" }, + { "Guldoman", "Core", "https://github.com/Guldoman" }, + { "liquidev", "Contributor", "https://github.com/liquidev" }, + { "Mat Mariani", "MacOS Support", "https://github.com/mathewmariani" }, + { "Nightwing", "Contributor", "https://github.com/Nightwing13" }, + { "Nils Kvist", "Contributor", "https://github.com/budRich" }, + { "Not-a-web-Developer", "Contributor", "https://github.com/Not-a-web-Developer" }, + { "Robert Štojs", "CI", "https://github.com/netrobert" }, + { "sammyette", "Plugins", "https://github.com/TorchedSammy" }, + { "Takase", "Core", "https://github.com/takase1121" }, + { "xwii", "Contributor", "https://github.com/xcb-xwii" } +} + + for _, c in ipairs(contributors_list) do + contributors:add_row({ + c[1], ListBox.COLEND, c[2], ListBox.COLEND, c[3] + }, c[3]) + end + + ---@param self widget + function self.about:update_positions() + local center = self:get_width() / 2 + + title:set_label("Lite XL") + title:set_position( + center - (title:get_width() / 2), + style.padding.y + ) + + version:set_position( + center - (version:get_width() / 2), + title:get_bottom() + (style.padding.y / 2) + ) + + description:set_position( + center - (description:get_width() / 2), + version:get_bottom() + (style.padding.y / 2) + ) + + button:set_position( + center - (button:get_width() / 2), + description:get_bottom() + style.padding.y + ) + + contributors:set_position( + style.padding.x, + button:get_bottom() + style.padding.y + ) + + contributors:set_size( + self:get_width() - (style.padding.x * 2), + self:get_height() - (button:get_bottom() + (style.padding.y * 2)) + ) + + contributors:set_visible_rows() + end +end + +---Reposition and resize core and plugin widgets. +function Settings:update() + if not Settings.super.update(self) then return end + + self.notebook:set_size(self.size.x, self.size.y) + + for _, section in ipairs({self.core_sections, self.plugin_sections}) do + if section.parent:is_visible() then + section:set_size( + section.parent.size.x - (style.padding.x), + section:get_real_height() + ) + section:set_position(style.padding.x / 2, 0) + for _, pane in ipairs(section.panes) do + local prev_child = nil + for pos=#pane.container.childs, 1, -1 do + local child = pane.container.childs[pos] + local x, y = 10, (10 * SCALE) + if prev_child then + if + (prev_child:is(Label) and not prev_child.desc) + or + (child:is(Label) and child.desc) + then + y = prev_child:get_bottom() + (10 * SCALE) + else + y = prev_child:get_bottom() + (30 * SCALE) + end + end + if child:is(Line) then + x = 0 + elseif child:is(ItemsList) or child:is(FilePicker) or child:is(TextBox) then + child:set_size(pane.container:get_width() - 20, child.size.y) + end + child:set_position(x, y) + prev_child = child + end + end + end + end + + if self.about:is_visible() then + self.about:update_positions() + end +end + +-------------------------------------------------------------------------------- +-- overwrite core run to inject previously saved settings +-------------------------------------------------------------------------------- +local core_run = core.run +function core.run() + store_default_keybindings() + + -- load plugins disabled by default and enabled by user + if settings.config.enabled_plugins then + for name, _ in pairs(settings.config.enabled_plugins) do + if + type(config.plugins[name]) == "boolean" + and + not config.plugins[name] + then + require("plugins." .. name) + end + end + end + + -- append all settings defined in the plugins spec + scan_plugins_spec() + + -- merge custom settings into config + merge_settings() + + ---@type settings.ui + settings.ui = Settings() + + -- apply user chosen color theme + if settings.config.theme and settings.config.theme ~= "default" then + core.try(function() + core.reload_module("colors." .. settings.config.theme) + end) + end + + -- re-apply user settings + core.load_user_directory() + core.load_project_module() + + core_run() +end + +-------------------------------------------------------------------------------- +-- Disable plugins at startup, only works if this file is the first +-- required on user module, or priority tag is obeyed by lite-xl. +-------------------------------------------------------------------------------- +-- load custom user settings that include list of disabled plugins +load_settings() + +-- only disable non already loaded plugins +if settings.config.disabled_plugins then + for name, _ in pairs(settings.config.disabled_plugins) do + if type(rawget(config.plugins, name)) == "nil" then + config.plugins[name] = false + end + end +end + +-- properly apply skip_plugins_version before other plugins are loaded +if settings.config.skip_plugins_version then + config.skip_plugins_version = true +else + config.skip_plugins_version = false +end + +-------------------------------------------------------------------------------- +-- Add command and keymap to load settings view +-------------------------------------------------------------------------------- +command.add(nil, { + ["ui:settings"] = function() + settings.ui:show() + local node = core.root_view:get_active_node_default() + local found = false + for _, view in ipairs(node.views) do + if view == settings.ui then + found = true + node:set_active_view(view) + break + end + end + if not found then + node:add_view(settings.ui) + end + end, +}) + +keymap.add { + ["ctrl+alt+p"] = "ui:settings" +} + +-------------------------------------------------------------------------------- +-- Overwrite toolbar preferences command to open the settings gui +-------------------------------------------------------------------------------- +if config.plugins.toolbarview ~= false then + local ToolbarView = require "plugins.toolbarview" + local toolbarview_on_mouse_moved = ToolbarView.on_mouse_moved + function ToolbarView:on_mouse_moved(px, py, ...) + toolbarview_on_mouse_moved(self, px, py, ...) + if + self.hovered_item + and + self.hovered_item.command == "core:open-user-module" + then + self.hovered_item.command = "ui:settings" + end + end +end + + +return settings; diff --git a/plugins/smallclock.lua b/plugins/smallclock.lua index f656a8f..e975152 100644 --- a/plugins/smallclock.lua +++ b/plugins/smallclock.lua @@ -1,27 +1,66 @@ --- mod-version:2 -- lite-xl 2.00 +-- mod-version:3 local core = require "core" +local config = require "core.config" +local common = require "core.common" local style = require "core.style" -local status_view = require "core.statusview" +local StatusView = require "core.statusview" -local time = "" - -core.add_thread(function() - while true do - local t = os.date("*t") - time = string.format("%02d:%02d", t.hour, t.min) - coroutine.yield(1) - end -end) - -local get_items = status_view.get_items +config.plugins.smallclock = common.merge({ + enabled = true, + clock_type = "24", + -- The config specification used by the settings gui + config_spec = { + name = "Small Clock", + { + label = "Enabled", + description = "Show or hide the small clock 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:small-clock"):show() + else + core.status_view:get_item("status:small-clock"):hide() + end + end) + end + }, + { + label = "Clock Type", + description = "Choose between 12 or 24 hours clock mode.", + path = "clock_type", + type = "selection", + default = "24", + values = { + {"24 Hours", "24"}, + {"12 Hours", "12"} + } + } + } +}, config.plugins.smallclock) -function status_view:get_items() - local left, right = get_items(self) - local t = {style.dim, self.separator2, style.accent, time} +local time = "" - for _, item in ipairs(t) do - table.insert(right, item) +local last_time = os.time() +local function update_time() + if os.time() > last_time then + local h = config.plugins.smallclock.clock_type == "24" + and os.date("%H") or os.date("%I") + local m = os.date("%M") + time = string.format("%02d:%02d", h, m) + last_time = os.time() end - - return left, right end + +core.status_view:add_item({ + name = "status:small-clock", + alignment = StatusView.Item.RIGHT, + get_item = function() + update_time() + return {style.accent, time} + end, + position = -1, + separator = core.status_view.separator2 +}) diff --git a/plugins/smoothcaret.lua b/plugins/smoothcaret.lua index 40d852e..5639ff2 100644 --- a/plugins/smoothcaret.lua +++ b/plugins/smoothcaret.lua @@ -1,15 +1,42 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local config = require "core.config" local style = require "core.style" +local common = require "core.common" local DocView = require "core.docview" -config.plugins.smoothcaret = { rate = 0.65 } +config.plugins.smoothcaret = common.merge({ + enabled = true, + rate = 0.65, + -- The config specification used by the settings gui + config_spec = { + name = "Smooth Caret", + { + label = "Enabled", + description = "Disable or enable the smooth caret animation.", + path = "enabled", + type = "toggle", + default = true + }, + { + label = "Rate", + description = "Speed of the animation.", + path = "rate", + type = "number", + default = 0.65, + min = 0.2, + max = 1.0, + step = 0.05 + }, + } +}, config.plugins.smoothcaret) local docview_update = DocView.update function DocView:update() docview_update(self) + if not config.plugins.smoothcaret.enabled then return end + local minline, maxline = self:get_visible_line_range() -- We need to keep track of all the carets @@ -21,10 +48,10 @@ function DocView:update() local idx, v_idx = 1, 1 for _, line, col in self.doc:get_selections() do - local x, y = self:get_line_screen_position(line) + local x, y = self:get_line_screen_position(line, col) -- Keep the position relative to the whole View -- This way scrolling won't animate the caret - x = x + self:get_col_x_offset(line, col) + self.scroll.x + x = x + self.scroll.x y = y + self.scroll.y if not self.carets[idx] then @@ -56,7 +83,7 @@ function DocView:update() -- Remove unused carets to avoid animating new ones when they are added for i = idx, #self.carets do - self.carets[idx] = nil + self.carets[i] = nil end if self.mouse_selecting ~= self.last_mouse_selecting then @@ -72,7 +99,13 @@ function DocView:update() self.caret_idx = 1 end +local docview_draw_caret = DocView.draw_caret function DocView:draw_caret(x, y) + if not config.plugins.smoothcaret.enabled then + docview_draw_caret(self, x, y) + return + end + local c = self.visible_carets[self.caret_idx] or { current = { x = x, y = y } } local lh = self:get_line_height() diff --git a/plugins/sort.lua b/plugins/sort.lua index 6c65149..1ad4034 100644 --- a/plugins/sort.lua +++ b/plugins/sort.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local translate = require "core.doc.translate" diff --git a/plugins/spellcheck.lua b/plugins/spellcheck.lua index 7b0ba2b..f55cf1f 100644 --- a/plugins/spellcheck.lua +++ b/plugins/spellcheck.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local style = require "core.style" local config = require "core.config" @@ -7,33 +7,42 @@ local common = require "core.common" local DocView = require "core.docview" local Doc = require "core.doc" -config.plugins.spellcheck = {} -config.plugins.spellcheck.files = { "%.txt$", "%.md$", "%.markdown$" } +local platform_dictionary_file if PLATFORM == "Windows" then - config.plugins.spellcheck.dictionary_file = EXEDIR .. "/words.txt" + platform_dictionary_file = EXEDIR .. "/words.txt" else - config.plugins.spellcheck.dictionary_file = "/usr/share/dict/words" + platform_dictionary_file = "/usr/share/dict/words" end +config.plugins.spellcheck = common.merge({ + enabled = true, + files = { "%.txt$", "%.md$", "%.markdown$" }, + dictionary_file = platform_dictionary_file +}, config.plugins.spellcheck) local last_input_time = 0 local word_pattern = "%a+" local words -core.add_thread(function() - local t = {} - local i = 0 - for line in io.lines(config.plugins.spellcheck.dictionary_file) do - for word in line:gmatch(word_pattern) do - t[word:lower()] = true +local function load_dictionary() + core.add_thread(function() + local t = {} + local i = 0 + for line in io.lines(config.plugins.spellcheck.dictionary_file) do + for word in line:gmatch(word_pattern) do + t[word:lower()] = true + end + i = i + 1 + if i % 1000 == 0 then coroutine.yield() end end - i = i + 1 - if i % 1000 == 0 then coroutine.yield() end - end - words = t - core.redraw = true - core.log_quiet("Finished loading dictionary file: \"%s\"", config.plugins.spellcheck.dictionary_file) -end) + words = t + core.redraw = true + core.log_quiet( + "Finished loading dictionary file: \"%s\"", + config.plugins.spellcheck.dictionary_file + ) + end) +end local function matches_any(filename, ptns) @@ -62,11 +71,16 @@ end local draw_line_text = DocView.draw_line_text function DocView:draw_line_text(idx, x, y) - draw_line_text(self, idx, x, y) - - if not words - or not matches_any(self.doc.filename or "", config.plugins.spellcheck.files) then - return + local lh = draw_line_text(self, idx, x, y) + + if + not config.plugins.spellcheck.enabled + or + not words + or + not matches_any(self.doc.filename or "", config.plugins.spellcheck.files) + then + return lh end local s, e = 0, 0 @@ -78,12 +92,13 @@ function DocView:draw_line_text(idx, x, y) local word = text:sub(s, e):lower() if not words[word] and not active_word(self.doc, idx, e + 1) then local color = style.spellcheck_error or style.syntax.keyword2 - local x1 = x + self:get_col_x_offset(idx, s) - local x2 = x + self:get_col_x_offset(idx, e + 1) + local x1, y1 = self:get_line_screen_position(idx, s) + local x2, y2 = self:get_line_screen_position(idx, e + 1) local h = math.ceil(1 * SCALE) - renderer.draw_rect(x1, y + self:get_line_height() - h, x2 - x1, h, color) + renderer.draw_rect(x1, y1 + self:get_line_height() - h, x2 - x1, h, color) end end + return lh end @@ -112,8 +127,44 @@ local function compare_words(word1, word2) end +-- The config specification used by the settings gui +config.plugins.spellcheck.config_spec = { + name = "Spell Check", + { + label = "Enabled", + description = "Disable or enable spell checking.", + path = "enabled", + type = "toggle", + default = true + }, + { + label = "Files", + description = "List of Lua patterns matching files to spell check.", + path = "files", + type = "list_strings", + default = { "%.txt$", "%.md$", "%.markdown$" } + }, + { + label = "Dictionary File", + description = "Path to a text file that contains a list of dictionary words.", + path = "dictionary_file", + type = "file", + exists = true, + default = platform_dictionary_file, + on_apply = function() + load_dictionary() + end + } +} + +load_dictionary() + command.add("core.docview", { + ["spell-check:toggle"] = function() + config.plugins.spellcheck.enabled = not config.plugins.spellcheck.enabled + end, + ["spell-check:add-to-dictionary"] = function() local word = get_word_at_caret() if words[word] then @@ -164,18 +215,28 @@ command.add("core.docview", { -- select word and init replacement selector local label = string.format("Replace \"%s\" With", word) doc:set_selection(line, e + 1, line, s) - core.command_view:enter(label, function(text, item) - text = item and item.text or text - doc:replace(function() return text end) - end, function(text) - local t = {} - for _, w in ipairs(suggestions) do - if w:lower():find(text:lower(), 1, true) then - table.insert(t, w) + core.command_view:enter(label, { + submit = function(text, item) + text = item and item.text or text + doc:replace(function() return text end) + end, + suggest = function(text) + local t = {} + for _, w in ipairs(suggestions) do + if w:lower():find(text:lower(), 1, true) then + table.insert(t, w) + end end + return t end - return t - end) + }) end, }) + +local contextmenu = require "plugins.contextmenu" +contextmenu:register("core.docview", { + contextmenu.DIVIDER, + { text = "View Suggestions", command = "spell-check:replace" }, + { text = "Add to Dictionary", command = "spell-check:add-to-dictionary" } +}) diff --git a/plugins/statusclock.lua b/plugins/statusclock.lua index 8289502..aec58e3 100644 --- a/plugins/statusclock.lua +++ b/plugins/statusclock.lua @@ -1,54 +1,88 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local config = require "core.config" local style = require "core.style" +local common = require "core.common" local StatusView = require "core.statusview" -local scan_rate = 1 -config.plugins.statusclock = { +config.plugins.statusclock = common.merge({ + enabled = true, time_format = "%H:%M:%S", - date_format = "%A, %d %B %Y" -} + date_format = "%A, %d %B %Y", + -- The config specification used by the settings gui + config_spec = { + name = "Status Clock", + { + label = "Enabled", + description = "Show or hide the clock 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:clock"):show() + else + core.status_view:get_item("status:clock"):hide() + end + end) + end + }, + { + label = "Time Format", + description = "Time specification defined with Lua date/time place holders.", + path = "time_format", + type = "string", + default = "%H:%M:%S" + }, + { + label = "Date Format", + description = "Date specification defined with Lua date/time place holders.", + path = "date_format", + type = "string", + default = "%A, %d %B %Y", + } + } +}, config.plugins.statusclock) local time_data = { time_text = '', date_text = '', } -core.add_thread(function() - while true do +local last_time = os.time() +local function update_time() + if os.time() > last_time then local time_text = os.date(config.plugins.statusclock.time_format) local date_text = os.date(config.plugins.statusclock.date_format) - + if time_data.time_text ~= time_text or time_data.time_text ~= date_text then - core.redraw = true time_data.time_text = time_text time_data.date_text = date_text end - - coroutine.yield(scan_rate) - end -end) - -local get_items = StatusView.get_items - -function StatusView:get_items() - local left, right = get_items(self) - - local t = { - style.dim, - self.separator, - style.dim and style.text, - time_data.date_text, - style.dim, - self.separator, - style.dim and style.text, - time_data.time_text, - } - for _, item in ipairs(t) do - table.insert(right, item) + -- only redraw if seconds enabled + if config.plugins.statusclock.time_format:find("%S", 1, true) then + core.redraw = true + end + last_time = os.time() end - - return left, right end +core.status_view:add_item({ + name = "status:clock", + alignment = StatusView.Item.RIGHT, + get_item = function(self) + update_time() + return { + style.text, + time_data.date_text, + style.dim, + self.separator, + style.text, + time_data.time_text, + } + end, + position = -1, + separator = core.status_view.separator2 +}) + diff --git a/plugins/tabnumbers.lua b/plugins/tabnumbers.lua index 581a6b2..97e5d10 100644 --- a/plugins/tabnumbers.lua +++ b/plugins/tabnumbers.lua @@ -1,28 +1,44 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 +local config = require "core.config" local common = require "core.common" -local core = require "core" local style = require "core.style" +local Node = require "core.node" --- quite hackish, but Node isn't normally public -local Node = getmetatable(core.root_view.root_node) -local draw_tabs = Node.draw_tabs +config.plugins.tabnumbers = common.merge({ + enabled = true, + -- The config specification used by the settings gui + config_spec = { + name = "Tab Numbers", + { + label = "Draw Tab Numbers", + description = "Show or hide numbers on the interface tabs.", + path = "enabled", + type = "toggle", + default = true + } + } +}, config.plugins.tabnumbers) -function Node:draw_tabs(...) - draw_tabs(self, ...) - - for i, view in ipairs(self.views) do - if i > 9 then break end - - local x, y, w, h = self:get_tab_rect(i) - local number = tostring(i) - local color = style.dim - local title_width = style.font:get_width(view:get_name()) - local free_real_estate = - math.min(math.max((w - title_width) / 2, style.padding.x), h) - if view == self.active_view then - color = style.accent +-- Overwrite draw_tab_title to prepend tab number +local Node_draw_tab_title = Node.draw_tab_title +function Node:draw_tab_title(view, font, is_active, is_hovered, x, y, w, h) + if config.plugins.tabnumbers.enabled then + local number = "" + for i, v in ipairs(self.views) do + if view == v then + number = tostring(i) + end + end + local padx = 0 + if number ~= "" then + padx = style.font:get_width(number) + (style.padding.x / 2) + w = w - padx + local color = is_active and style.text or style.dim + common.draw_text(style.font, color, number, nil, x, y, w, h) end - -- renderer.draw_rect(x, y + h - 1, free_real_estate, 1, color) - common.draw_text(style.font, color, tostring(i), "center", x, y, free_real_estate, h) + local tx = x + padx -- Space for number + Node_draw_tab_title(self, view, font, is_active, is_hovered, tx, y, w, h) + else + Node_draw_tab_title(self, view, font, is_active, is_hovered, x, y, w, h) end end diff --git a/plugins/texcompile.lua b/plugins/texcompile.lua index 0c19d1b..af31aed 100644 --- a/plugins/texcompile.lua +++ b/plugins/texcompile.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local config = require "core.config" local command = require "core.command" diff --git a/plugins/themeselect.lua b/plugins/themeselect.lua index 70bd627..fe4ff34 100644 --- a/plugins/themeselect.lua +++ b/plugins/themeselect.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" -- Load a specific theme when the filename of an active document does match @@ -7,12 +7,11 @@ local core = require "core" -- usage: -- require("plugins.themeselect").add_pattern("%.md$", "summer") -local theme_select = { } +local theme_select = {} local saved_colors_module = "core.style" -local themes_patterns = { -} +local themes_patterns = {} local reload_module = core.reload_module local set_visited = core.set_visited diff --git a/plugins/titleize.lua b/plugins/titleize.lua index edb50d2..16d20b0 100644 --- a/plugins/titleize.lua +++ b/plugins/titleize.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" @@ -9,4 +9,3 @@ command.add("core.docview", { end) end, }) - diff --git a/plugins/togglesnakecamel.lua b/plugins/togglesnakecamel.lua index b5d20d9..c055933 100644 --- a/plugins/togglesnakecamel.lua +++ b/plugins/togglesnakecamel.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" diff --git a/plugins/typingspeed.lua b/plugins/typingspeed.lua index 62e44ef..0e28ec9 100644 --- a/plugins/typingspeed.lua +++ b/plugins/typingspeed.lua @@ -1,4 +1,4 @@ --- mod-version:2 +-- mod-version:3 local core = require "core" local style = require "core.style" @@ -6,17 +6,38 @@ local common = require "core.common" local config = require "core.config" local DocView = require "core.docview" -if common["merge"] then - config.plugins.typingspeed = common.merge({ - -- characters that should be counted as word boundary - word_boundaries = "[%p%s]", - }, config.plugins.keystats) -else - config.plugins.typingspeed = { - -- characters that should be counted as word boundary - word_boundaries = "[%p%s]", - } -end +config.plugins.typingspeed = common.merge({ + enabled = true, + -- characters that should be counted as word boundary + word_boundaries = "[%p%s]", + -- The config specification used by the settings gui + config_spec = { + name = "Typing Speed", + { + label = "Enabled", + description = "Show or hide the typing speed 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("typing-speed:stats"):show() + else + core.status_view:get_item("typing-speed:stats"):hide() + end + end) + end + }, + { + label = "Word Boundaries", + description = "Lua pattern that matches characters to separate words.", + path = "word_boundaries", + type = "string", + default = "[%p%s]" + } + } +}, config.plugins.typingspeed) local chars = 0 local chars_last = 0 @@ -29,65 +50,51 @@ local wpm = 0 core.add_thread(function() while true do - local t = os.date("*t") - if t.sec <= time_last then - words_last = words - words = 0 - chars_last = chars - chars = 0 - time_last = t.sec - end - wpm = words_last * (1-(t.sec)/60) + words - cpm = chars_last * (1-(t.sec)/60) + chars + if config.plugins.typingspeed.enabled then + local t = os.date("*t") + if t.sec <= time_last then + words_last = words + words = 0 + chars_last = chars + chars = 0 + time_last = t.sec + end + wpm = words_last * (1-(t.sec)/60) + words + cpm = chars_last * (1-(t.sec)/60) + chars + end coroutine.yield(1) end end) local on_text_input = DocView.on_text_input function DocView:on_text_input(text, idx) - chars = chars + 1 - if string.find(text, config.plugins.typingspeed.word_boundaries) then - if started_word then - words = words + 1 - started_word = false - end - else - started_word = true - end + if config.plugins.typingspeed.enabled then + chars = chars + 1 + if string.find(text, config.plugins.typingspeed.word_boundaries) then + if started_word then + words = words + 1 + started_word = false + end + else + started_word = true + end + end on_text_input(self, text, idx) end -if core.status_view["add_item"] then - core.status_view:add_item( - function() - return core.active_view and getmetatable(core.active_view) == DocView - end, - "keystats:stats", - core.status_view.Item.RIGHT, - function() - return { - style.text, - string.format("%.0f CPM / %.0f WPM", cpm, wpm) - } - end, - nil, - 1, - "characters / words per minute" - ).separator = core.status_view.separator2 -else - local get_items = core.status_view.get_items - function core.status_view:get_items() - local left, right = get_items(self) - - local t = { - style.text, string.format("%.0f CPM / %.0f WPM", cpm, wpm), - style.dim, self.separator2 - } - - for i, item in ipairs(t) do - table.insert(right, i, item) - end - - return left, right - end -end +core.status_view:add_item({ + predicate = function() + return core.active_view and getmetatable(core.active_view) == DocView + end, + name = "typing-speed:stats", + alignment = core.status_view.Item.RIGHT, + get_item = function() + return { + style.text, + string.format("%.0f CPM / %.0f WPM", cpm, wpm) + } + end, + position = 1, + tooltip = "characters / words per minute", + separator = core.status_view.separator2 +}) diff --git a/plugins/unboundedscroll.lua b/plugins/unboundedscroll.lua index a27ab25..0793cfa 100644 --- a/plugins/unboundedscroll.lua +++ b/plugins/unboundedscroll.lua @@ -1,6 +1,18 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 +local command = require "core.command" local DocView = require "core.docview" -function DocView.clamp_scroll_position() - -- do nothing -end +local doc_view_clamp_scroll_position = DocView.clamp_scroll_position +local function clamp_scroll_noop() end + +DocView.clamp_scroll_position = clamp_scroll_noop + +command.add(nil, { + ["unbounded-scroll:toggle"] = function() + if DocView.clamp_scroll_position == clamp_scroll_noop then + DocView.clamp_scroll_position = doc_view_clamp_scroll_position + else + DocView.clamp_scroll_position = clamp_scroll_noop + end + end, +}) diff --git a/plugins/wordcount.lua b/plugins/wordcount.lua new file mode 100644 index 0000000..42224ac --- /dev/null +++ b/plugins/wordcount.lua @@ -0,0 +1,90 @@ +-- mod-version:3 +local core = require "core" +local style = require "core.style" +local StatusView = require "core.statusview" +local CommandView = require "core.commandview" +local DocView = require "core.docview" +local Doc = require "core.doc" +local keymap = require "core.keymap" + + +local words = setmetatable({}, { __mode = "k" }) + + +local function compute_line_words(line) + local s, total_words = 1, 0 + while true do + local ns, e = line:find("%s+", s) + if ns == 1 and e == #line then break end + if not e then total_words = math.max(total_words, 1) break end + total_words = total_words + 1 + s = e + 1 + end + return total_words +end + + +local function compute_words(doc, start_line, end_line) + local total_words = 0 + for i = start_line or 1, end_line or #doc.lines do + total_words = total_words + compute_line_words(doc.lines[i]) + end + return total_words +end + + +local old_raw_insert = Doc.raw_insert +function Doc:raw_insert(line, col, text, undo_stack, time) + if words[self] then + local old_count = compute_words(self, line, line) + old_raw_insert(self, line, col, text, undo_stack, time) + local total_lines, s = 0, 0 + while true do + s = text:find("\n", s + 1, true) + if not s then break end + total_lines = total_lines + 1 + end + words[self] = words[self] + compute_words(self, line, line + total_lines) - old_count + else + old_raw_insert(self, line, col, text, undo_stack, time) + end +end + + +local old_raw_remove = Doc.raw_remove +function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) + if words[self] then + local old_count = compute_words(self, line1, line2) + old_raw_remove(self, line1, col1, line2, col2, undo_stack, time) + words[self] = words[self] + compute_words(self, line1, line1) - old_count + else + old_raw_remove(self, line1, col1, line2, col2, undo_stack, time) + end +end + + +local old_doc_new = Doc.new +function Doc:new(...) + old_doc_new(self, ...) + words[self] = compute_words(self) +end + +local cached_word_length, cached_word_count + +core.status_view:add_item({ + predicate = function() return core.active_view:is(DocView) and not core.active_view:is(CommandView) and words[core.active_view.doc] end, + name = "status:word-count", + alignment = StatusView.Item.RIGHT, + get_item = function() + local selection_text = core.active_view.doc:get_selection_text() + if #selection_text ~= cached_word_length then + cached_word_count = compute_line_words(selection_text) + cached_word_length = #selection_text + end + if #selection_text > 0 then + return { style.text, cached_word_count .. " words" } + else + return { style.text, words[core.active_view.doc] .. " words" } + end + end +}) |