diff options
-rw-r--r-- | README.md | 6 | ||||
-rw-r--r-- | manifest.json | 87 | ||||
-rw-r--r-- | plugins/colorpicker.lua | 56 | ||||
-rw-r--r-- | plugins/custom_caret.lua | 73 | ||||
-rw-r--r-- | plugins/keymap_export.lua | 265 | ||||
-rw-r--r-- | plugins/language_rivet.lua | 34 | ||||
-rw-r--r-- | plugins/minimap.lua | 33 | ||||
-rw-r--r-- | plugins/search_ui.lua | 979 | ||||
-rw-r--r-- | plugins/settings.lua | 14 | ||||
-rw-r--r-- | plugins/svg_screenshot.lua | 345 |
10 files changed, 1766 insertions, 126 deletions
@@ -50,9 +50,9 @@ but only with a `url` must provide a `checksum` that matches the existing plugin | [`bracketmatch`](plugins/bracketmatch.lua?raw=1) | Underlines matching pair for bracket under the caret *([screenshot](https://user-images.githubusercontent.com/3920290/80132745-0c863f00-8594-11ea-8875-c455c6fd7eae.png))* | | [`build`](https://github.com/adamharrison/lite-xl-ide.git)\* | Provides a build system, messages window, and easily clickable errors. Supports an internal build system, and `make`. *([screenshot](https://raw.githubusercontent.com/adamharrison/lite-xl-ide/main/screenshots/build.png))* | | [`centerdoc`](plugins/centerdoc.lua?raw=1) | Centers document's content on the screen and adds zen mode support *([screenshot](https://user-images.githubusercontent.com/3920290/82127896-bf6e4500-97ae-11ea-97fc-ba9a552bc9a4.png))* | +| [`colorpicker`](plugins/colorpicker.lua?raw=1) | Color picker dialog that supports html and rgb notations. | | [`colorpreview`](plugins/colorpreview.lua?raw=1) | Underlays color values (eg. `#ff00ff` or `rgb(255, 0, 255)`) with their resultant color. *([screenshot](https://user-images.githubusercontent.com/3920290/80743752-731bd780-8b15-11ea-97d3-847db927c5dc.png))* | | [`console`](https://github.com/franko/console)\* | A console for running external commands and capturing their output *([gif](https://user-images.githubusercontent.com/3920290/81343656-49325a00-90ad-11ea-8647-ff39d8f1d730.gif))* | -| [`contextmenu`](https://github.com/takase1121/lite-contextmenu)\* | Simple context menu *([screenshot](https://github.com/takase1121/lite-contextmenu/blob/master/assets/screenshot.jpg?raw=true))* | | [`copyfilelocation`](plugins/copyfilelocation.lua?raw=1) | Copy file location to clipboard | | [`custom_caret`](plugins/custom_caret.lua?raw=1) | Customize the caret in the editor | | [`datetimestamps`](plugins/datetimestamps.lua?raw=1) | Insert date-, time- and date-time-stamps | @@ -83,6 +83,7 @@ but only with a `url` must provide a `checksum` that matches the existing plugin | [`indent_convert`](plugins/indent_convert.lua?raw=1) | Convert between tabs and spaces indentation | | [`indentguide`](plugins/indentguide.lua?raw=1) | Adds indent guides *([screenshot](https://user-images.githubusercontent.com/3920290/79640716-f9860000-818a-11ea-9c3b-26d10dd0e0c0.png))* | | [`ipc`](plugins/ipc.lua?raw=1) | Adds inter-process communication support | +| [`keymap_export`](plugins/keymap_export.lua?raw=1) | Exports the keymap to a JSON file. | | [`Kinc Projects`](https://github.com/Kode-Community/kinc_plugin)\* | Adds [Kinc](https://github.com/Kode/Kinc) Project generation with basic build commands(depends on [`console`](https://github.com/franko/console)) | | [`language_angelscript`](plugins/language_angelscript.lua?raw=1) | Syntax for the [Angelscript](https://www.angelcode.com/angelscript/) programming language | | [`language_assembly_x86`](plugins/language_assembly_x86.lua?raw=1) | Syntax for Intel x86 assembly | @@ -187,14 +188,17 @@ but only with a `url` must provide a `checksum` that matches the existing plugin | [`regexreplacepreview`](plugins/regexreplacepreview.lua?raw=1) | Allows for you to write a regex and its replacement in one go, and live preview the results. | | [`restoretabs`](plugins/restoretabs.lua?raw=1) | Keep a list of recently closed tabs, and restore the tab in order on ctrl+shift+t. | | [`scalestatus`](plugins/scalestatus.lua?raw=1) | Displays current scale (zoom) in status view (depends on scale plugin) | +| [`search_ui`](plugins/search_ui.lua?raw=1) | Friendlier search and replace user interface using Widgets. | | [`select_colorscheme`](plugins/select_colorscheme.lua?raw=1) | Select a color theme, like VScode, Sublime Text.(plugin saves changes) | | [`selectionhighlight`](plugins/selectionhighlight.lua?raw=1) | Highlights regions of code that match the current selection *([screenshot](https://user-images.githubusercontent.com/3920290/80710883-5f597c80-8ae7-11ea-97f0-76dfacc08439.png))* | | [`settings`](plugins/settings.lua?raw=1) | Provides a GUI to manage core and plugin settings, bindings and select color theme *([video](https://user-images.githubusercontent.com/1702572/169743674-ececae24-f6b7-4ff2-bfa2-c4762cd327d9.mp4))*. (depends on [`widget`](https://github.com/lite-xl/lite-xl-widgets)) | | [`smallclock`](plugins/smallclock.lua?raw=1) | Displays the current time in the corner of the status view | | [`smoothcaret`](plugins/smoothcaret.lua?raw=1) | Smooth caret animation *([gif](https://user-images.githubusercontent.com/20792268/139006049-a0ba6559-88cb-49a7-8077-4822445b4a1f.gif))* | | [`sort`](plugins/sort.lua?raw=1) | Sorts selected lines alphabetically | +| [`Source Control Management`](https://github.com/lite-xl/lite-xl-scm)\* | Extensible source control management plugin with git and fossil backends. | | [`spellcheck`](plugins/spellcheck.lua?raw=1) | Underlines misspelt words *([screenshot](https://user-images.githubusercontent.com/3920290/79923973-9caa7400-842e-11ea-85d4-7a196a91ca50.png))* *-- note: on Windows a [`words.txt`](https://github.com/dwyl/english-words/blob/master/words.txt) dictionary file must be placed beside the exe* | | [`statusclock`](plugins/statusclock.lua?raw=1) | Displays the current date and time in the corner of the status view | +| [`svg_screenshot`](plugins/svg_screenshot.lua?raw=1) | Takes an SVG screenshot. Only browsers seem to support the generated SVG properly. | | [`tab_switcher`](plugins/tab_switcher.lua?raw=1) | Switch between open tabs by searching by name | | [`tabnumbers`](plugins/tabnumbers.lua?raw=1) | Displays tab numbers from 1–9 next to their names \*([screenshot](https://user-images.githubusercontent.com/16415678/101285362-007a8500-37e5-11eb-869b-c10eb9d9d902.png)) | | [`texcompile`](plugins/texcompile.lua?raw=1) | Compile Tex files into PDF | diff --git a/manifest.json b/manifest.json index a6e3f03..c75361f 100644 --- a/manifest.json +++ b/manifest.json @@ -1,5 +1,5 @@ { - "plugins": [ + "addons": [ { "description": "Align multiple carets and selections *([clip](https://user-images.githubusercontent.com/2798487/165631951-532f8d24-d596-4dd0-9d21-ff53c71ed32f.mp4))*", "version": "0.1", @@ -59,7 +59,7 @@ { "description": "Provides a build system, messages window, and easily clickable errors. Supports an internal build system, and `make`. *([screenshot](https://raw.githubusercontent.com/adamharrison/lite-xl-ide/main/screenshots/build.png))*", "version": "0.1", - "remote": "https://github.com/adamharrison/lite-xl-ide.git:445345f2b25898404879dacbd7709f8a7c7b0795", + "remote": "https://github.com/adamharrison/lite-xl-ide.git:c254d8cbc1932fd69e4c135f1d53c4e81a9f293a", "id": "build", "mod_version": "3" }, @@ -71,6 +71,14 @@ "mod_version": "3" }, { + "description": "Color picker dialog that supports html and rgb notations.", + "version": "0.1", + "path": "plugins/colorpicker.lua", + "id": "colorpicker", + "mod_version": "3", + "dependencies": { "widget": {} } + }, + { "description": "Underlays color values (eg. `#ff00ff` or `rgb(255, 0, 255)`) with their resultant color. *([screenshot](https://user-images.githubusercontent.com/3920290/80743752-731bd780-8b15-11ea-97d3-847db927c5dc.png))*", "version": "0.1", "path": "plugins/colorpreview.lua", @@ -85,13 +93,6 @@ "id": "console" }, { - "description": "Simple context menu *([screenshot](https://github.com/takase1121/lite-contextmenu/blob/master/assets/screenshot.jpg?raw=true))*", - "version": "0.1", - "remote": "https://github.com/takase1121/lite-contextmenu:5af041bf27319c6c26316c7fc8b7a60494be7d32", - "mod_version": "3", - "id": "contextmenu" - }, - { "description": "Copy file location to clipboard", "version": "0.1", "path": "plugins/copyfilelocation.lua", @@ -100,7 +101,7 @@ }, { "description": "Customize the caret in the editor", - "version": "0.1", + "version": "0.2", "path": "plugins/custom_caret.lua", "id": "custom_caret", "mod_version": "3" @@ -122,7 +123,7 @@ { "description": "Provides a debugger integration, with pluggable backends. Currently supports only gdb. *([screenshot](https://raw.githubusercontent.com/adamharrison/lite-xl-ide/main/screenshots/debugger.png))*", "version": "0.1", - "remote": "https://github.com/adamharrison/lite-xl-ide.git:445345f2b25898404879dacbd7709f8a7c7b0795", + "remote": "https://github.com/adamharrison/lite-xl-ide.git:c254d8cbc1932fd69e4c135f1d53c4e81a9f293a", "id": "debugger", "mod_version": "3" }, @@ -151,14 +152,14 @@ "description": "Add support for detecting file and string encodings as converting between them.", "version": "1.1", "type": "library", - "remote": "https://github.com/jgmdev/lite-xl-encoding:b1ddf226277ea12a03ed9db2ddda458988020e91", + "remote": "https://github.com/jgmdev/lite-xl-encoding:16e2477e916f52e18f6d63f5ac61ace58b0c5e45", "mod_version": "3", "id": "encoding" }, { "description": "Properly read files that are not encoded in UTF-8 or ASCII by auto-detecting their encoding and allows saving on different text encodings.", "version": "1.0", - "remote": "https://github.com/jgmdev/lite-xl-encoding:b1ddf226277ea12a03ed9db2ddda458988020e91", + "remote": "https://github.com/jgmdev/lite-xl-encoding:16e2477e916f52e18f6d63f5ac61ace58b0c5e45", "mod_version": "3", "id": "encodings", "dependencies": { "encoding": { } } @@ -231,7 +232,7 @@ "name": "Multithreaded Find File", "description": "Threaded project find files.", "version": "1.0", - "remote": "https://github.com/jgmdev/lite-xl-threads:e61ffd28fc852b143fe468c4b43c68d605f22335", + "remote": "https://github.com/jgmdev/lite-xl-threads:9299a9a6b778cb34b12f0286b9162779920a9197", "mod_version": "3", "id": "findfileimproved", "dependencies": { "thread": { } } @@ -321,6 +322,13 @@ "mod_version": "3" }, { + "description": "Exports the keymap to a JSON file.", + "version": "0.1", + "path": "plugins/keymap_export.lua", + "id": "keymap_export", + "mod_version": "3" + }, + { "description": "Adds [Kinc](https://github.com/Kode/Kinc) Project generation with basic build commands(depends on [`console`](https://github.com/franko/console))", "version": "0.1", "remote": "https://github.com/Kode-Community/kinc_plugin:309fe4193a09cf739ed0a058b1a6966a463a1dbd", @@ -860,8 +868,8 @@ }, { "description": "Advanced linter with ErrorLens-like error reporting. Compatible with linters made for `linter` *([screenshot](https://raw.githubusercontent.com/liquid600pgm/lintplus/master/screenshots/1.png))*", - "version": "0.1", - "remote": "https://github.com/liquid600pgm/lintplus:3268641818069070b270486a88966b2a8bfef97e", + "version": "0.2", + "remote": "https://github.com/liquid600pgm/lintplus:771b1fe6cddb7897cd034ed5ee96201d6a2831c2", "mod_version": "3", "id": "lintplus", "name": "lint+" @@ -889,8 +897,8 @@ }, { "description": "Provides code completion (also known as IntelliSense) using the Language Server Protocol", - "version": "0.1", - "remote": "https://github.com/lite-xl/lite-xl-lsp:a6a8f70d6304bd77c7588e0a652945002df7fbad", + "version": "0.2", + "remote": "https://github.com/lite-xl/lite-xl-lsp:dc37d18c91d3243f9d7530364d8c24a3da8446fa", "mod_version": "3", "id": "lsp" }, @@ -924,7 +932,7 @@ }, { "description": "Shows a minimap on the right-hand side of the docview. Taken from [@andsve](https://github.com/andsve/lite-plugins/tree/minimap-plugin), and improved upon.", - "version": "0.1", + "version": "0.2", "path": "plugins/minimap.lua", "id": "minimap", "mod_version": "3" @@ -947,7 +955,7 @@ "description": "Add support for TCP and UDP sockets using SDL_net.", "version": "1.1", "type": "library", - "remote": "https://github.com/jgmdev/lite-xl-net:a1930395c89e24344db686f2e83ce67a602c5dbf", + "remote": "https://github.com/jgmdev/lite-xl-net:4ddece50cdc6d00ab09be1896ef0474e89da89b8", "mod_version": "3", "id": "net" }, @@ -1018,7 +1026,7 @@ "name": "Multithreaded Project Search", "description": "Threaded project search with 5-10x better performance.", "version": "1.2", - "remote": "https://github.com/jgmdev/lite-xl-threads:e61ffd28fc852b143fe468c4b43c68d605f22335", + "remote": "https://github.com/jgmdev/lite-xl-threads:9299a9a6b778cb34b12f0286b9162779920a9197", "mod_version": "3", "id": "projectsearch", "dependencies": { "thread": { } } @@ -1052,6 +1060,23 @@ "mod_version": "3" }, { + "name": "Source Control Management", + "description": "Extensible source control management plugin with git and fossil backends.", + "version": "0.1", + "remote": "https://github.com/lite-xl/lite-xl-scm:930951990f9a3c78178265e5380e3c9e40b109d2", + "id": "scm", + "mod_version": "3", + "dependencies": { "widget": {} } + }, + { + "description": "Friendlier search and replace user interface using Widgets.", + "version": "0.1", + "path": "plugins/search_ui.lua", + "id": "search_ui", + "mod_version": "3", + "dependencies": { "widget": {} } + }, + { "description": "Select a color theme, like VScode, Sublime Text.(plugin saves changes)", "version": "0.1", "path": "plugins/select_colorscheme.lua", @@ -1067,10 +1092,11 @@ }, { "description": "Provides a GUI to manage core and plugin settings, bindings and select color theme *([video](https://user-images.githubusercontent.com/1702572/169743674-ececae24-f6b7-4ff2-bfa2-c4762cd327d9.mp4))*. (depends on [`widget`](https://github.com/lite-xl/lite-xl-widgets))", - "version": "0.4", + "version": "0.5", "path": "plugins/settings.lua", "id": "settings", - "mod_version": "3" + "mod_version": "3", + "dependencies": { "widget": {} } }, { "description": "Displays the current time in the corner of the status view", @@ -1108,6 +1134,13 @@ "mod_version": "3" }, { + "description": "Takes an SVG screenshot. Only browsers seem to support the generated SVG properly.", + "version": "0.1", + "path": "plugins/svg_screenshot.lua", + "id": "svg_screenshot", + "mod_version": "3" + }, + { "description": "Switch between open tabs by searching by name", "version": "0.1", "path": "plugins/tab_switcher.lua", @@ -1169,9 +1202,9 @@ { "name": "Threads", "description": "Adds the missing multithreading functionality.", - "version": "1.1", + "version": "1.3", "type": "library", - "remote": "https://github.com/jgmdev/lite-xl-threads:e61ffd28fc852b143fe468c4b43c68d605f22335", + "remote": "https://github.com/jgmdev/lite-xl-threads:9299a9a6b778cb34b12f0286b9162779920a9197", "mod_version": "3", "id": "thread" }, @@ -1219,8 +1252,8 @@ }, { "description": "Plugin library that provides a set of re-usable components to more easily write UI elements for your plugins", - "version": "0.1", - "remote": "https://github.com/lite-xl/lite-xl-widgets:a632bfdf7c66bacc272fe2c962621cd9860058e1", + "version": "0.2", + "remote": "https://github.com/lite-xl/lite-xl-widgets:4c29ff3f89fb2988a7169094a554fee7972c9803", "mod_version": "3", "id": "widget" }, diff --git a/plugins/colorpicker.lua b/plugins/colorpicker.lua new file mode 100644 index 0000000..c280fb6 --- /dev/null +++ b/plugins/colorpicker.lua @@ -0,0 +1,56 @@ +-- mod-version:3 +local command = require "core.command" +local keymap = require "core.keymap" +local ColorPickerDialog = require "libraries.widget.colorpickerdialog" + +---Get the color format of given text. +---@param text string +---@return "html" | "html_opacity" | "rgb" +local function get_color_type(text) + local found = text:find("#%x%x%x%x%x%x%x?%x?") + if found then + found = text:find("#%x%x%x%x%x%x%x%x") + if found then return "html_opacity" end + return "html" + else + found = text:find("#%x%x%x") + if found then + return "html" + else + found = text:find( + "rgba?%((%d+)%D+(%d+)%D+(%d+)[%s,]-([%.%d]-)%s-%)" + ) + if found then return "rgb" end + end + end + return "html" +end + +command.add("core.docview!", { + ["color-picker:open"] = function(dv) + ---@type core.doc + local doc = dv.doc + local selection = doc:get_text(doc:get_selection()) + local type = get_color_type(selection) + + ---@type widget.colorpickerdialog + local picker = ColorPickerDialog(nil, selection) + function picker:on_apply(c) + local value + if type == "html" then + value = string.format("#%02X%02X%02X", c[1], c[2], c[3]) + elseif type == "html_opacity" then + value = string.format("#%02X%02X%02X%02X", c[1], c[2], c[3], c[4]) + elseif type == "rgb" then + value = string.format("rgba(%d, %d, %d, %.2f)", c[1], c[2], c[3], c[4]/255) + end + doc:text_input(value) + end + picker:show() + picker:centered() + end, +}) + +keymap.add { + ["ctrl+alt+k"] = "color-picker:open" +} diff --git a/plugins/custom_caret.lua b/plugins/custom_caret.lua index f0358cd..a8d0601 100644 --- a/plugins/custom_caret.lua +++ b/plugins/custom_caret.lua @@ -29,10 +29,7 @@ local DocView = require "core.docview" config.plugins.custom_caret = common.merge({ shape = "line", custom_color = true, - color_r = style.caret[1], - color_g = style.caret[2], - color_b = style.caret[3], - opacity = style.caret[4] + caret_color = table.pack(table.unpack(style.caret)) }, config.plugins.custom_caret) -- Reference to plugin config @@ -41,20 +38,17 @@ local conf = config.plugins.custom_caret -- Get real default caret color after everything is loaded up core.add_thread(function() if - conf.color_r == 147 and conf.color_g == 221 + conf.caret_color[1] == 147 and conf.caret_color[2] == 221 and - conf.color_b == 250 and conf.opacity == 255 + conf.caret_color[3] == 250 and conf.caret_color[4] == 255 and ( - style.caret[1] ~= conf.color_r or style.caret[2] ~= conf.color_g + style.caret[1] ~= conf.caret_color[1] or style.caret[2] ~= conf.caret_color[2] or - style.caret[3] ~= conf.color_b or style.caret[4] ~= conf.opacity + style.caret[3] ~= conf.caret_color[3] or style.caret[4] ~= conf.caret_color[4] ) then - conf.color_r = style.caret[1] - conf.color_g = style.caret[2] - conf.color_b = style.caret[3] - conf.opacity = style.caret[4] + conf.caret_color = table.pack(table.unpack(style.caret)) end local settings_loaded, settings = pcall(require, "plugins.settings") @@ -81,48 +75,12 @@ core.add_thread(function() default = true }, { - label = "Red Component of Color", - description = "The color consists of 3 components RGB, " - .. "This modifies the 'R' component of the caret's color", - path = "color_r", - type = "number", - min = 0, - max = 255, - default = style.caret[1], - step = 1, - }, - { - label = "Green Component of Color", - description = "The color consists of 3 components RGB, " - .. "This modifies the 'G' component of the caret's color", - path = "color_g", - type = "number", - min = 0, - max = 255, - default = style.caret[2], - step = 1, - }, - { - label = "Blue Component of Color", - description = "The color consists of 3 components RGB, " - .. "This modifies the 'B' component of the caret's color", - path = "color_b", - type = "number", - min = 0, - max = 255, - default = style.caret[3], - step = 1, - }, - { - label = "Opacity of the Cursor", - description = "The Opacity of the caret", - path = "opacity", - type = "number", - min = 0, - max = 255, - default = style.caret[4], - step = 1, - }, + label = "Caret Color", + description = "Custom color of the caret.", + path = "caret_color", + type = "color", + default = table.pack(table.unpack(style.caret)), + } } ---@cast settings plugins.settings @@ -134,12 +92,7 @@ function DocView:draw_caret(x, y) local caret_width = style.caret_width local caret_height = self:get_line_height() local current_caret_shape = conf.shape - local caret_color = conf.custom_color and { - conf.color_r, - conf.color_g, - conf.color_b, - conf.opacity - } or style.caret + local caret_color = conf.custom_color and conf.caret_color or style.caret if (current_caret_shape == "block") then caret_width = math.ceil(self:get_font():get_width("a")) diff --git a/plugins/keymap_export.lua b/plugins/keymap_export.lua new file mode 100644 index 0000000..64635b9 --- /dev/null +++ b/plugins/keymap_export.lua @@ -0,0 +1,265 @@ +-- mod-version:3 + +-- Author: Takase (takase1121) +-- Description: Exports the keymap into a JSON file. +-- License: MIT + +-- This file contains source code modified from https://github.com/rxi/json.lua +-- The source code is under MIT and the license is at the end of this file. + +local core = require "core" +local common = require "core.common" +local command = require "core.command" +local config = require "core.config" +local keymap = require "core.keymap" + +-- not configurable via config for obvious reasons +local QUIT_AFTER_EXPORT = false + +config.plugins.keymap_export = common.merge({ + export_type = "reverse_map", + destination = "doc", + allow_env = true, + autostart = false, + config_spec = { + name = "Keymap export", + { + label = "Export type", + description = "Which part of the keymap to export.", + path = "export_type", + type = "selection", + default = "reverse_map", + values = { + { "Map", "map" }, + { "Reverse map", "reverse_map" } + } + }, + { + label = "Export destination", + description = "The destination. Set to 'doc' or a filename.", + path = "destination", + type = "string", + default = "doc" + }, + { + label = "Allow environment variables", + description = "Allow using environment variables to modify config.", + path = "allow_env", + type = "toggle", + default = true + }, + { + label = "Autostart", + description = "Automatically export on Lite XL startup.", + path = "autostart", + type = "toggle", + default = false + } + } +}, config.plugins.keymap_export) + +local conf = config.plugins.keymap_export + +----------------------------------------------------------- +-- START OF json.lua +----------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + +----------------------------------------------------------- +-- END OF json.lua +----------------------------------------------------------- + +-- convert all strings into arrays +local function normalize_value(v) + return type(v) == "string" and ({ v }) or v +end + +local function export_keymap() + local copy_map = {} + -- copy the keymap into a temporary table so we can sort it + for k, v in pairs(keymap[conf.export_type]) do + copy_map[#copy_map + 1] = { k, normalize_value(v) } + end + table.sort(copy_map, function(a, b) return a[1] < b[1] end) + local output = encode(copy_map) + + if conf.destination == "doc" then + -- open a doc containing the keymap so users can save it separately + local d = core.open_doc(conf.export_type) + core.root_view:open_doc(d) + d:insert(1, 1, output) + d.new_file = false + d:clean() + else + -- export into a file + local f, err = io.open(conf.destination, "w") + if not f then + core.error("cannot write to output: %s", err) + return + end + + f:write(output) + f:close() + end + + core.log("Keymap exported to %s.", conf.destination) + + if QUIT_AFTER_EXPORT then + core.quit(true) + end +end + +command.add(nil, { + ["keymap:export"] = export_keymap +}) + + +core.add_thread(function() + -- have to wait for the editor to start up!!!! + -- or else settings will override this + if conf.allow_env then + -- check the following envs to override some settings + if os.getenv("KEYMAP_EXPORT_TYPE") ~= nil then + conf.export_type = os.getenv("KEYMAP_EXPORT_TYPE") + end + if os.getenv("KEYMAP_EXPORT_DESTINATION") ~= nil then + conf.destination = os.getenv("KEYMAP_EXPORT_DESTINATION") + end + if os.getenv("KEYMAP_EXPORT_AUTOSTART") ~= nil then + conf.autostart = os.getenv("KEYMAP_EXPORT_AUTOSTART") == "true" + end + if os.getenv("KEYMAP_EXPORT_QUIT_AFTER_EXPORT") ~= nil then + QUIT_AFTER_EXPORT = os.getenv("KEYMAP_EXPORT_QUIT_AFTER_EXPORT") == "true" + end + end + + if conf.autostart then + export_keymap() + end +end) + + +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- diff --git a/plugins/language_rivet.lua b/plugins/language_rivet.lua index aceb5a1..cbd6bde 100644 --- a/plugins/language_rivet.lua +++ b/plugins/language_rivet.lua @@ -35,6 +35,7 @@ syntax.add { {pattern = "#%s?[%a_][%w_]*", type = "comment"} -- if/elif/else/endif }, symbols = { + ["alias"] = "keyword", ["and"] = "keyword", ["as"] = "keyword", ["base"] = "literal", @@ -51,19 +52,17 @@ syntax.add { ["extern"] = "keyword", ["export"] = "keyword", ["false"] = "literal", - ["fn"] = "keyword", + ["func"] = "keyword", ["for"] = "keyword", ["from"] = "keyword", ["if"] = "keyword", ["import"] = "keyword", ["in"] = "keyword", ["is"] = "keyword", - ["let"] = "keyword", ["mut"] = "keyword", ["nil"] = "literal", ["or"] = "keyword", - ["pub"] = "keyword", - ["prot"] = "keyword", + ["public"] = "keyword", ["return"] = "keyword", ["self"] = "literal", ["struct"] = "keyword", @@ -71,26 +70,29 @@ syntax.add { ["test"] = "keyword", ["trait"] = "keyword", ["true"] = "literal", - ["type"] = "keyword", ["unsafe"] = "keyword", + ["var"] = "keyword", ["while"] = "keyword", -- types ["never"] = "keyword2", - ["void"] = "keyword2", ["bool"] = "keyword2", - ["i8"] = "keyword2", - ["i16"] = "keyword2", - ["i32"] = "keyword2", - ["i64"] = "keyword2", + ["comptime_int"] = "keyword2", + ["comptime_float"] = "keyword2", + ["int8"] = "keyword2", + ["int16"] = "keyword2", + ["int32"] = "keyword2", + ["int64"] = "keyword2", ["isize"] = "keyword2", - ["u8"] = "keyword2", - ["u16"] = "keyword2", - ["u32"] = "keyword2", - ["u64"] = "keyword2", + ["uint8"] = "keyword2", + ["uint16"] = "keyword2", + ["uint32"] = "keyword2", + ["uint64"] = "keyword2", ["usize"] = "keyword2", - ["f32"] = "keyword2", - ["f64"] = "keyword2", + ["float32"] = "keyword2", + ["float64"] = "keyword2", + ["anyptr"] = "keyword2", + ["mut_anyptr"] = "keyword2", ["rune"] = "keyword2", ["string"] = "keyword2", ["Base"] = "keyword2", diff --git a/plugins/minimap.lua b/plugins/minimap.lua index 75eb272..ac94849 100644 --- a/plugins/minimap.lua +++ b/plugins/minimap.lua @@ -135,29 +135,21 @@ config.plugins.minimap = common.merge({ }, { label = "Selection Color", - description = "Background color of selected text in html notation eg: #FFFFFF. Leave empty to use default.", - path = "selection_color_html", - type = "string", - on_apply = function(value) - if value and value:match("#%x%x%x%x%x%x") then - config.plugins.minimap.selection_color = { common.color(value) } - else - config.plugins.minimap.selection_color = nil - end - end + description = "Background color of selected text.", + path = "selection_color", + type = "color", + default = string.format("#%02X%02X%02X%02X", + style.dim[1], style.dim[2], style.dim[3], style.dim[4] + ) }, { label = "Caret Color", - description = "Background color of active line in html notation eg: #FFFFFF. Leave empty to use default.", - path = "caret_color_html", - type = "string", - on_apply = function(value) - if value and value:match("#%x%x%x%x%x%x") then - config.plugins.minimap.caret_color = { common.color(value) } - else - config.plugins.minimap.caret_color = nil - end - end + description = "Background color of active line.", + path = "caret_color", + type = "color", + default = string.format("#%02X%02X%02X%02X", + style.caret[1], style.caret[2], style.caret[3], style.caret[4] + ) }, { label = "Highlight Alignment", @@ -663,4 +655,3 @@ command.add("core.docview!", { }) return MiniMap - diff --git a/plugins/search_ui.lua b/plugins/search_ui.lua new file mode 100644 index 0000000..e030184 --- /dev/null +++ b/plugins/search_ui.lua @@ -0,0 +1,979 @@ +-- mod-version:3 +-- +-- Replacement for the find/replace and project search CommandView +---interface using Widgets with some extra features. +-- @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 keymap = require "core.keymap" +local search = require "core.doc.search" +local projectsearch = require "plugins.projectsearch" +local CommandView = require "core.commandview" +local DocView = require "core.docview" +local Widget = require "libraries.widget" +local Button = require "libraries.widget.button" +local CheckBox = require "libraries.widget.checkbox" +local Line = require "libraries.widget.line" +local Label = require "libraries.widget.label" +local TextBox = require "libraries.widget.textbox" +local SelectBox = require "libraries.widget.selectbox" +local FilePicker = require "libraries.widget.filepicker" + +---@class config.plugins.search_ui +---@field replace_core_find boolean +---@field position "right" | "bottom" +config.plugins.search_ui = common.merge({ + replace_core_find = true, + position = "bottom", + config_spec = { + name = "Search User Interface", + { + label = "Replace Core Find", + description = "Replaces the core find view when using the find shortcut.", + path = "replace_core_find", + type = "toggle", + default = true + }, + { + label = "Position", + description = "Location of search interface.", + path = "position", + type = "selection", + default = "bottom", + values = { + { "Top", "top" }, + { "Right", "right" }, + { "Bottom", "bottom" } + } + } + } +}, config.plugins.search_ui) + +---@type core.docview +local doc_view = nil + +---@type widget +local widget = Widget(nil, false) +widget.name = "Search and Replace" +widget:set_border_width(0) +widget.scrollable = true +widget:hide() +widget.init_size = true + +---@type widget.label +local label = Label(widget, "Find and Replace") +label:set_position(10, 10) + +---@type widget.line +local line = Line(widget) +line:set_position(0, label:get_bottom() + 10) + +---@type widget.textbox +local findtext = TextBox(widget, "", "search...") +findtext:set_position(10, line:get_bottom() + 10) +findtext:set_tooltip("Text to search") + +---@type widget.textbox +local replacetext = TextBox(widget, "", "replacement...") +replacetext:set_position(10, findtext:get_bottom() + 10) +replacetext:set_tooltip("Text to replace") + +---@type widget.button +local findprev = Button(widget, "") +findprev:set_icon("<") +findprev:set_position(10, replacetext:get_bottom() + 10) +findprev:set_tooltip("Find previous") + +---@type widget.button +local findnext = Button(widget, "") +findnext:set_icon(">") +findnext:set_position(findprev:get_right() + 5, replacetext:get_bottom() + 10) +findnext:set_tooltip("Find next") + +---@type widget.button +local findproject = Button(widget, "Find") +findproject:set_icon("L") +findproject:set_position(findprev:get_right() + 5, replacetext:get_bottom() + 10) +findproject:set_tooltip("Find in project") +findproject:hide() + +---@type widget.button +local replace = Button(widget, "Replace") +replace:set_position(10, findnext:get_bottom() + 10) +replace:set_tooltip("Replace all matching results") + +---@type widget.line +local line_options = Line(widget) +line_options:set_position(0, replace:get_bottom() + 10) + +---@type widget.checkbox +local insensitive = CheckBox(widget, "Insensitive") +insensitive:set_position(10, line_options:get_bottom() + 10) +insensitive:set_tooltip("Case insensitive search") +insensitive:set_checked(true) + +---@type widget.checkbox +local patterncheck = CheckBox(widget, "Pattern") +patterncheck:set_position(10, insensitive:get_bottom() + 10) +patterncheck:set_tooltip("Treat search text as a lua pattern") + +---@type widget.checkbox +local regexcheck = CheckBox(widget, "Regex") +regexcheck:set_position(10, patterncheck:get_bottom() + 10) +regexcheck:set_tooltip("Treat search text as a regular expression") + +---@type widget.checkbox +local replaceinselection = CheckBox(widget, "Replace in Selection") +replaceinselection:set_position(10, regexcheck:get_bottom() + 10) +replaceinselection:set_tooltip("Perform replace only on selected text") + +---@type widget.selectbox +local scope = SelectBox(widget, "scope") +scope:set_position(10, regexcheck:get_bottom() + 10) +scope:add_option("current file") +scope:add_option("project files") +scope:set_selected(1) + +---@type widget.filepicker +local filepicker = FilePicker(widget) +filepicker:set_mode(FilePicker.mode.DIRECTORY) +filepicker:set_position(10, scope:get_bottom() + 10) +filepicker:set_tooltip("Directory to perform the search") +filepicker:hide() + +---@type widget.line +local statusline = Line(widget) +statusline:set_position(0, scope:get_bottom() + 10) + +---@type widget.label +local status = Label(widget, "") +status:set_position(10, statusline:get_bottom() + 10) + +-------------------------------------------------------------------------------- +-- Helper class to keep track of amount of matches and display on status label +-------------------------------------------------------------------------------- +---@class plugins.search_ui.result +---@field line integer +---@field col integer + +---@class plugins.search_ui.results +---@field text string +---@field matches plugins.search_ui.result[] +---@field doc core.doc? +local Results = { + text = "", + matches = {}, + doc = nil, + prev_search_id = 0 +} + +---@param text string +---@param doc core.doc +function Results:find(text, doc, force) + if self.text == text and self.doc == doc and not force then + self:set_status() + return + end + + -- disable previous search thread + if self.prev_search_id > 0 and core.threads[self.prev_search_id] then + core.threads[self.prev_search_id] = { + cr = coroutine.create(function() end), wake = 0 + } + end + + self.text = text + self.doc = doc + + local search_func + + -- regex search + if regexcheck:is_checked() then + local regex_find_offsets = regex.match + if regex.find_offsets then + regex_find_offsets = regex.find_offsets + end + local pattern = regex.compile( + findtext:get_text(), + insensitive:is_checked() and "im" or "m" + ) + if not pattern then return end + search_func = function(line_text) + ---@cast line_text string + local results = nil + local offsets = {regex_find_offsets(pattern, line_text)} + if offsets[1] then + results = {} + for i=1, #offsets, 2 do + table.insert(results, offsets[i]) + end + end + return results + end + -- plain or pattern search + else + local no_case = insensitive:is_checked() + local is_plain = not patterncheck:is_checked() + if is_plain and no_case then + text = text:ulower() + end + search_func = function(line_text) + ---@cast line_text string + if is_plain and no_case then + line_text = line_text:ulower() + end + local results = nil + local col1, col2 = line_text:find(text, 1, is_plain) + if col1 then + results = {} + table.insert(results, col1) + while col1 do + col1, col2 = line_text:find(text, col2+1, is_plain) + if col1 then + table.insert(results, col1) + end + end + end + return results + end + end + + self.prev_search_id = core.add_thread(function() + self.matches = {} + local lines_count = #doc.lines + for i=1, lines_count do + local offsets = search_func(doc.lines[i]) + if offsets then + for _, col in ipairs(offsets) do + table.insert(self.matches, {line = i, col = col}) + end + end + if i % 100 == 0 then + coroutine.yield() + end + end + self:set_status() + end) +end + +---@return integer +function Results:current() + if not self.doc then return 0 end + local line1, col1, line2, col2 = self.doc:get_selection() + if line1 == line2 and col1 == col2 then return 0 end + local line = math.min(line1, line2) + local col = math.min(col1, col2) + if self.matches and #self.matches > 0 then + for i, result in ipairs(self.matches) do + if result.line == line and result.col == col then + return i + end + end + end + return 0 +end + +function Results:clear() + self.text = "" + self.matches = {} + self.doc = nil + status:set_label("") +end + +function Results:set_status() + local current = self:current() + local total = self.matches and #self.matches or 0 + if total > 0 then + status:set_label( + "Result: " .. tostring(current .. " of " .. tostring(total)) + ) + else + status:set_label("") + end +end + +-------------------------------------------------------------------------------- +-- Helper functions +-------------------------------------------------------------------------------- +local function view_is_open(target_view) + if not target_view then return false end + local found = false + for _, view in ipairs(core.root_view.root_node:get_children()) do + if view == target_view then + found = true + break + end + end + return found +end + +local function toggle_scope(idx, not_set) + if not not_set then scope:set_selected(idx) end + + if idx == 1 then + replacetext:show() + findnext:show() + findprev:show() + replace:show() + patterncheck:show() + replaceinselection:show() + findproject:hide() + filepicker:hide() + + if view_is_open(doc_view) and findtext:get_text() ~= "" then + Results:find(findtext:get_text(), doc_view.doc) + else + Results:clear() + end + else + replacetext:hide() + findnext:hide() + findprev:hide() + replace:hide() + patterncheck:hide() + replaceinselection:hide() + findproject:show() + filepicker:show() + + Results:clear() + end +end + +local function project_search() + if findtext:get_text() == "" then return end + if not regexcheck:is_checked() then + projectsearch.search_plain( + findtext:get_text(), filepicker:get_path(), insensitive:is_checked() + ) + else + projectsearch.search_regex( + findtext:get_text(), filepicker:get_path(), insensitive:is_checked() + ) + end + command.perform "search-replace:hide" +end + +local find_enabled = true +local function find(reverse) + if + not view_is_open(doc_view) or findtext:get_text() == "" or not find_enabled + then + Results:clear() + return + end + + if core.last_active_view and core.last_active_view:is(DocView) then + doc_view = core.last_active_view + end + + local doc = doc_view.doc + local cline1, ccol1, cline2, ccol2 = doc:get_selection() + local line, col = cline1, ccol1 + if reverse and ccol2 < ccol1 then + col = ccol2 + end + + local opt = { + wrap = true, + no_case = insensitive:is_checked(), + pattern = patterncheck:is_checked(), + regex = regexcheck:is_checked(), + reverse = reverse + } + + if opt.regex and not regex.compile(findtext:get_text()) then + return + end + + status:set_label("") + + core.try(function() + local line1, col1, line2, col2 = search.find( + doc, line, col, findtext:get_text(), opt + ) + + local current_text = doc:get_text( + table.unpack({ doc:get_selection() }) + ) + + if opt.no_case and not opt.regex and not opt.pattern then + current_text = current_text:ulower() + end + + if line1 then + local text = findtext:get_text() + if opt.no_case and not opt.regex and not opt.pattern then + text = text:ulower() + end + if reverse or (current_text == text or current_text == "") then + doc:set_selection(line1, col2, line2, col1) + else + doc:set_selection(line1, col1, line2, col2) + end + doc_view:scroll_to_line(line1, true) + Results:find(text, doc) + end + end) +end + +local function find_replace() + if core.last_active_view:is(DocView) then + doc_view = core.last_active_view + end + local doc = doc_view.doc + + if not replaceinselection:is_checked() then + local line1, col1, line2, col2 = doc:get_selection() + if line1 ~= line2 or col1 ~= col2 then + doc:set_selection(line1, col1) + end + end + + local old = findtext:get_text() + local new = replacetext:get_text() + + local results = doc:replace(function(text) + if not regexcheck:is_checked() then + if not patterncheck:is_checked() then + return text:gsub(old:gsub("%W", "%%%1"), new:gsub("%%", "%%%%"), nil) + else + return text:gsub(old, new) + end + end + local result, matches = regex.gsub(regex.compile(old, "m"), text, new) + if type(matches) == "table" then + return result, #matches + end + return result, matches + end) + + local n = 0 + for _,v in pairs(results) do + n = n + v + end + + status:set_label(string.format("Total Replaced: %d", n)) +end + +local inside_node = false +local current_node = nil +local current_position = "" + +local function add_to_node() + if not inside_node or current_position ~= config.plugins.search_ui.position then + if + current_position ~= "" + and + current_position ~= config.plugins.search_ui.position + then + widget:hide() + current_node:remove_view(core.root_view.root_node, widget) + core.root_view.root_node:update_layout() + widget:set_size(0, 0) + widget.init_size = true + end + local node = core.root_view:get_primary_node() + if config.plugins.search_ui.position == "right" then + current_node = node:split("right", widget, {x=true}, true) + current_position = "right" + elseif config.plugins.search_ui.position == "top" then + current_node = node:split("up", widget, {y=true}, false) + current_position = "top" + else + current_node = node:split("down", widget, {y=true}, false) + current_position = "bottom" + end + widget:show() + inside_node = true + end +end + +---Show or hide the search pane. +---@param av? core.docview +---@param toggle? boolean +local function show_find(av, toggle) + if + not view_is_open(av) + and + scope:get_selected() == 1 + then + widget:swap_active_child() + if config.plugins.search_ui.position == "right" then + widget:hide_animated(false, true) + else + widget:hide_animated(true, false) + end + return + end + + if inside_node and current_position == config.plugins.search_ui.position then + if toggle then + widget:toggle_visible(true, false, true) + else + if not widget:is_visible() then + if config.plugins.search_ui.position == "right" then + widget:show_animated(false, true) + else + widget:show_animated(true, false) + end + end + end + else + add_to_node() + end + + if widget:is_visible() then + status:set_label("") + + widget:swap_active_child(findtext) + doc_view = av + if view_is_open(doc_view) and doc_view.doc then + local doc_text = doc_view.doc:get_text( + table.unpack({ doc_view.doc:get_selection() }) + ) + if insensitive:is_checked() then doc_text = doc_text:ulower() end + local current_text = findtext:get_text() + if insensitive:is_checked() then current_text = current_text:ulower() end + if doc_text and doc_text ~= "" and current_text ~= doc_text then + local original_text = doc_view.doc:get_text( + table.unpack({ doc_view.doc:get_selection() }) + ) + find_enabled = false + findtext:set_text(original_text) + find_enabled = true + elseif current_text ~= "" and doc_text == "" then + if scope:get_selected() == 1 then + find(false) + end + end + if findtext:get_text() ~= "" then + findtext.textview.doc:set_selection(1, math.huge, 1, 1) + if scope:get_selected() == 1 then + Results:find(findtext:get_text(), doc_view.doc) + else + Results:clear() + end + else + Results:clear() + end + end + else + widget:swap_active_child() + if view_is_open(doc_view) then + core.set_active_view(doc_view) + end + end +end + +-------------------------------------------------------------------------------- +-- Widgets event overrides +-------------------------------------------------------------------------------- +function findtext:on_change(text) + if scope:get_selected() == 1 then + find(false) + end +end + +function insensitive:on_checked(checked) + Results:clear() +end + +function patterncheck:on_checked(checked) + if checked then + regexcheck:set_checked(false) + end + Results:clear() +end + +function regexcheck:on_checked(checked) + if checked then + patterncheck:set_checked(false) + end + Results:clear() +end + +function scope:on_selected(idx) + toggle_scope(idx, true) + if not view_is_open(doc_view) and idx == 1 then + command.perform "search-replace:hide" + end +end + +function findnext:on_click() find(false) end +function findprev:on_click() find(true) end +function findproject:on_click() project_search() end +function replace:on_click() find_replace() end + +---@param self widget +local function update_size(self) + if config.plugins.search_ui.position == "right" then + if scope:get_selected() == 1 then + if self.size.x < replace:get_right() + replace:get_width() / 2 then + self.size.x = replace:get_right() + replace:get_width() / 2 + end + else + if self.size.x < findproject:get_right() + findproject:get_width() * 2 then + self.size.x = findproject:get_right() + findproject:get_width() * 2 + end + end + else + self:set_size(nil, self:get_real_height() + 10) + end +end + +---@param self widget +local function update_right_positioning(self) + scope:show() + label:show() + status:show() + line_options:show() + label:set_label("Find and Replace") + + label:set_position(10, 10) + line:set_position(0, label:get_bottom() + 10) + findtext:set_position(10, line:get_bottom() + 10) + findtext.size.x = self.size.x - 20 + + if scope:get_selected() == 1 then + replacetext:set_position(10, findtext:get_bottom() + 10) + replacetext.size.x = self.size.x - 20 + findprev:set_position(10, replacetext:get_bottom() + 10) + findnext:set_position(findprev:get_right() + 5, replacetext:get_bottom() + 10) + replace:set_position(findnext:get_right() + 5, replacetext:get_bottom() + 10) + line_options:set_position(0, replace:get_bottom() + 10) + else + findproject:set_position(10, findtext:get_bottom() + 10) + replace:set_position(findproject:get_right() + 5, replacetext:get_bottom() + 10) + line_options:set_position(0, findproject:get_bottom() + 10) + end + + insensitive:set_position(10, line_options:get_bottom() + 10) + if scope:get_selected() == 1 then + patterncheck:set_position(10, insensitive:get_bottom() + 10) + regexcheck:set_position(10, patterncheck:get_bottom() + 10) + replaceinselection:set_position(10, regexcheck:get_bottom() + 10) + scope:set_position(10, replaceinselection:get_bottom() + 10) + else + regexcheck:set_position(10, insensitive:get_bottom() + 10) + scope:set_position(10, regexcheck:get_bottom() + 10) + end + + scope:set_size(self.size.x - 20) + if scope:get_selected() == 1 then + statusline:set_position(0, scope:get_bottom() + 30) + else + filepicker:set_position(10, scope:get_bottom() + 10) + filepicker:set_size(self.size.x - 20, nil) + statusline:set_position(0, filepicker:get_bottom() + 30) + end + + status:set_position(10, statusline:get_bottom() + 10) + if status.label == "" then + statusline:hide() + else + statusline:show() + end + + if self.init_size then + update_size(self) + self.init_size = false + self:show_animated(false, true) + end + + add_to_node() +end + +---@param self widget +local function update_bottom_positioning(self) + scope:hide() + statusline:hide() + + if scope:get_selected() == 1 then + label:hide() + status:show() + status:set_position(10, 10) + replaceinselection:set_position(self.size.x - replaceinselection:get_width() - 10, 10) + regexcheck:set_position(replaceinselection:get_position().x - 10 - regexcheck:get_width(), 10) + patterncheck:set_position(regexcheck:get_position().x - 10 - patterncheck:get_width(), 10) + insensitive:set_position(patterncheck:get_position().x - 10 - insensitive:get_width(), 10) + line:set_position(0, status:get_bottom() + 10) + else + label:show() + status:hide() + label:set_label("Find in Directory") + label:set_position(10, 10) + regexcheck:set_position(self.size.x - regexcheck:get_width() - 10, 10) + insensitive:set_position(regexcheck:get_position().x - 10 - insensitive:get_width(), 10) + line:set_position(0, label:get_bottom() + 10) + end + + if scope:get_selected() == 1 then + findtext:set_position(10, line:get_bottom() + 10) + findtext.size.x = self.size.x - 40 - findprev:get_width() - findnext:get_width() + findnext:set_position(self.size.x - 10 - findnext:get_width(), line:get_bottom() + 10) + findprev:set_position(findnext:get_position().x - 10 - findprev:get_width(), line:get_bottom() + 10) + replacetext:set_position(10, findtext:get_bottom() + 10) + replacetext.size.x = findtext.size.x + replace:set_position(self.size.x - 15 - replace:get_width(), findtext:get_bottom() + 10) + replace.size.x = findprev:get_width() + findnext:get_width() + 10 + line_options:hide() + else + findtext:set_position(10, line:get_bottom() + 10) + findtext.size.x = self.size.x - 30 - findproject:get_width() + findproject:set_position(self.size.x - 10 - findproject:get_width(), line:get_bottom() + 10) + replace:set_position(findproject:get_right() + 5, replacetext:get_bottom() + 10) + line_options:show() + line_options:set_position(0, findproject:get_bottom() + 10) + filepicker:set_position(10, line_options:get_bottom() + 10) + filepicker:set_size(self.size.x - 20, nil) + end + + if self.init_size then + update_size(self) + self.init_size = false + self:show_animated(true, false) + end + + add_to_node() +end + +-- reposition items on scale changes +function widget:update() + if Widget.update(self) then + if config.plugins.search_ui.position == "right" then + update_right_positioning(self) + else + update_bottom_positioning(self) + end + end +end + +function widget:on_scale_change(...) + Widget.on_scale_change(self, ...) + update_size(self) +end + +-------------------------------------------------------------------------------- +-- Override set_active_view to keep track of currently active docview +-------------------------------------------------------------------------------- +local core_set_active_view = core.set_active_view +function core.set_active_view(...) + core_set_active_view(...) + local view = core.next_active_view or core.active_view + if + view ~= doc_view + and + widget:is_visible() + and + view:extends(DocView) + and + view ~= findtext.textview + and + view ~= replacetext.textview + and + view.doc.filename + then + doc_view = view + Results:clear() + end +end + +-------------------------------------------------------------------------------- +-- Register commands +-------------------------------------------------------------------------------- +command.add( + function() + if core.active_view:is(DocView) then + return true, core.active_view + elseif widget:is_visible() then + return true, doc_view + elseif scope:get_selected() == 2 then + return true, nil + end + return false + end, + { + ["search-replace:show"] = function(av) + show_find(av, false) + end, + + ["search-replace:toggle"] = function(av) + show_find(av, true) + end + } +) + +command.add(function() return widget:is_visible() and not core.active_view:is(CommandView) end, { + ["search-replace:hide"] = function() + widget:swap_active_child() + if config.plugins.search_ui.position == "right" then + widget:hide_animated(false, true) + else + widget:hide_animated(true, false) + end + if view_is_open(doc_view) then + core.set_active_view(doc_view) + end + end, + + ["search-replace:file-search"] = function() + toggle_scope(1) + command.perform "search-replace:show" + end, + + ["search-replace:next"] = function() + find(false) + end, + + ["search-replace:previous"] = function() + find(true) + end, + + ["search-replace:toggle-sensitivity"] = function() + insensitive:set_checked(not insensitive:is_checked()) + Results:clear() + end, + + ["search-replace:toggle-regex"] = function() + regexcheck:set_checked(not regexcheck:is_checked()) + Results:clear() + end, + + ["search-replace:toggle-in-selection"] = function() + replaceinselection:set_checked(not replaceinselection:is_checked()) + end +}) + +command.add( + function() + return widget:is_visible() + and + not core.active_view:is(CommandView) + and + ( + widget.child_active == findtext + or + widget.child_active == replacetext + ) + end, + { + ["search-replace:perform"] = function() + if scope:get_selected() == 1 then + if widget.child_active == findtext then + ---@type core.doc + local doc = doc_view.doc + local line1, col1, line2, col2 = doc:get_selection() + -- correct cursor position to properly search next result + if line1 ~= line2 or col1 ~= col2 then + doc:set_selection( + line1, + math.max(col1, col2), + line2, + math.min(col1, col2) + ) + end + find(false) + else + find_replace() + end + else + project_search() + end + end + } +) + +-------------------------------------------------------------------------------- +-- Override core find/replace commands +-------------------------------------------------------------------------------- +local find_replace_find = command.map["find-replace:find"].perform +command.map["find-replace:find"].perform = function(...) + if config.plugins.search_ui.replace_core_find then + toggle_scope(1) + command.perform "search-replace:show" + else + find_replace_find(...) + end +end + +local find_replace_replace = command.map["find-replace:replace"].perform +command.map["find-replace:replace"].perform = function(...) + if config.plugins.search_ui.replace_core_find then + toggle_scope(1) + command.perform "search-replace:show" + else + find_replace_replace(...) + end +end + +local find_replace_repeat = command.map["find-replace:repeat-find"].perform +command.map["find-replace:repeat-find"].perform = function(...) + if + widget:is_visible() + or + (config.plugins.search_ui.replace_core_find and findtext:get_text() ~= "") + then + find(false) + return + end + find_replace_repeat(...) +end + +local find_replace_previous = command.map["find-replace:previous-find"].perform +command.map["find-replace:previous-find"].perform = function(...) + if + widget:is_visible() + or + (config.plugins.search_ui.replace_core_find and findtext:get_text() ~= "") + then + find(true) + return + end + find_replace_previous(...) +end + +local project_search_find = command.map["project-search:find"].perform +command.map["project-search:find"].perform = function(path) + if config.plugins.search_ui.replace_core_find then + toggle_scope(2) + if path then + filepicker:set_path(path) + end + local av = doc_view + if + core.active_view:extends(DocView) + and + core.active_view ~= findtext.textview + and + core.active_view ~= replacetext.textview + then + av = core.active_view + end + show_find(av, false) + return + end + project_search_find(path) +end + +-------------------------------------------------------------------------------- +-- Register keymaps +-------------------------------------------------------------------------------- +keymap.add { + ["alt+h"] = "search-replace:toggle", + ["escape"] = "search-replace:hide", + ["f3"] = "search-replace:next", + ["shift+f3"] = "search-replace:previous", + ["return"] = "search-replace:perform", + ["shift+return"] = "search-replace:previous", + ["ctrl+i"] = "search-replace:toggle-sensitivity", + ["ctrl+shift+i"] = "search-replace:toggle-regex", + ["ctrl+alt+i"] = "search-replace:toggle-in-selection", + ["ctrl+f"] = "search-replace:file-search" +} + + +return widget diff --git a/plugins/settings.lua b/plugins/settings.lua index d6c524e..d3af077 100644 --- a/plugins/settings.lua +++ b/plugins/settings.lua @@ -30,6 +30,7 @@ local ItemsList = require "libraries.widget.itemslist" local KeybindingDialog = require "libraries.widget.keybinddialog" local Fonts = require "libraries.widget.fonts" local FilePicker = require "libraries.widget.filepicker" +local ColorPicker = require "libraries.widget.colorpicker" local MessageBox = require "libraries.widget.messagebox" ---@class plugins.settings @@ -53,7 +54,8 @@ settings.type = { BUTTON = 6, FONT = 7, FILE = 8, - DIRECTORY = 9 + DIRECTORY = 9, + COLOR = 10 } ---@alias settings.types @@ -65,6 +67,8 @@ settings.type = { ---| `settings.type.BUTTON` ---| `settings.type.FONT` ---| `settings.type.FILE` +---| `settings.type.DIRECTORY` +---| `settings.type.COLOR` ---Represents a setting to render on a settings pane. ---@class settings.option @@ -1261,6 +1265,14 @@ local function add_control(pane, option, plugin_name) file.filters = option.filters or {} widget = file found = true + + elseif option.type == settings.type.COLOR then + ---@type widget.label + Label(pane, option.label .. ":") + ---@type widget.colorpicker + local color = ColorPicker(pane, option_value) + widget = color + found = true end if widget and type(path) ~= "nil" then diff --git a/plugins/svg_screenshot.lua b/plugins/svg_screenshot.lua new file mode 100644 index 0000000..87da77c --- /dev/null +++ b/plugins/svg_screenshot.lua @@ -0,0 +1,345 @@ +-- mod-version:3 + +--[[ + base64 -- v1.5.3 public domain Lua base64 encoder/decoder + no warranty implied; use at your own risk + Needs bit32.extract function. If not present it's implemented using BitOp + or Lua 5.3 native bit operators. For Lua 5.1 fallbacks to pure Lua + implementation inspired by Rici Lake's post: + http://ricilake.blogspot.co.uk/2007/10/iterating-bits-in-lua.html + author: Ilya Kolbin (iskolbin@gmail.com) + url: github.com/iskolbin/lbase64 + COMPATIBILITY + Lua 5.1+, LuaJIT + LICENSE + See end of file for license information. +--]] + +-- This utility has been altered to remove unused functionality + +--[[ +------------------------------------------------------------------------------ +License for the base64 utility +------------------------------------------------------------------------------ +MIT License +Copyright (c) 2018 Ilya Kolbin +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +--]] + +local base64 = {} + +local extract = function( v, from, width ) + return ( v >> from ) & ((1 << width) - 1) +end + + +function base64.makeencoder( s62, s63, spad ) + local encoder = {} + for b64code, char in pairs{[0]='A','B','C','D','E','F','G','H','I','J', + 'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y', + 'Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n', + 'o','p','q','r','s','t','u','v','w','x','y','z','0','1','2', + '3','4','5','6','7','8','9',s62 or '+',s63 or'/',spad or'='} do + encoder[b64code] = char:byte() + end + return encoder +end + +local DEFAULT_ENCODER = base64.makeencoder() + +local char, concat = string.char, table.concat + +function base64.encode( str, encoder, usecaching ) + encoder = encoder or DEFAULT_ENCODER + local t, k, n = {}, 1, #str + local lastn = n % 3 + local cache = {} + for i = 1, n-lastn, 3 do + local a, b, c = str:byte( i, i+2 ) + local v = a*0x10000 + b*0x100 + c + local s + if usecaching then + s = cache[v] + if not s then + s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)]) + cache[v] = s + end + else + s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)]) + end + t[k] = s + k = k + 1 + end + if lastn == 2 then + local a, b = str:byte( n-1, n ) + local v = a*0x10000 + b*0x100 + t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[64]) + elseif lastn == 1 then + local v = str:byte( n )*0x10000 + t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[64], encoder[64]) + end + return concat( t ) +end + +-------------------------------------------------------------------------------- + +local core = require "core" +local common = require "core.common" +local keymap = require "core.keymap" +local command = require "core.command" +local style = require "core.style" + +-- TODO: what about the vertical location of text? (svg uses the baseline) +-- TODO: add the overrides only when screenshotting to avoid overhead +-- TODO: complete the font table + +local start_screenshot = false +local screenshotting = false +local draw_data = {} +local known_fonts = {} +local known_colors = {} +local current_clip = "" +local known_clips = {} + +local function is_color(t) + if type(t) ~= "table" then return false end + if #t ~=4 then return false end + for i=1,4 do + if type(t[i]) ~= "number" then return false end + end + return true +end + +local function get_color(color) + return "rgba(" .. table.concat(color, ",") .. ")" +end + +local function get_fill_color(color) + if known_colors[color] then + return "var(--lxl_".. known_colors[color] .. ")" + end + + local fill_color = get_color(color) + -- Try to find a known color with the same values + for k, v in pairs(known_colors) do + if get_color(k) == fill_color then + -- Save the color with the name of the found color + known_colors[color] = v + return get_fill_color(k) + end + end + -- Try to find the color with a different opacity + local opaque_color = {table.unpack(color)} + opaque_color[4] = 255 + local opaque_fill_color = get_color(opaque_color) + for k, _ in pairs(known_colors) do + if get_color(k) == opaque_fill_color then + -- Hacky way to reuse the defined color with a custom opacity + return get_fill_color(k) .. '" opacity="' .. color[4]/255 + end + end + -- Logging warning next frame to avoid drawing it in the screenshot + core.add_thread(function() + core.warn("Unknown color: %s", common.serialize(color)) + end) + return fill_color +end + +local old_begin_frame = renderer.begin_frame +function renderer.begin_frame(...) + if start_screenshot then + start_screenshot = false + screenshotting = true + known_fonts = {} + current_clip = "" + known_clips = {} + -- `shape-rendering="crispEdges"` is needed to avoid antialisaing issues like + -- spaces between rects + -- `font-variant-ligatures: none;` is needed because we don't support ligatures, + -- so the svg shouldn't too + table.insert(draw_data, string.format([[ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="%d" height="%d" viewBox="0 0 %d %d" xmlns="http://www.w3.org/2000/svg" version="1.1" shape-rendering="crispEdges"> +<style> + * { + font-variant-ligatures: none; + } +</style>]], core.root_view.size.x, core.root_view.size.y, core.root_view.size.x, core.root_view.size.y)) + -- Extract known colors + known_colors = {} + local colors = {} + for k, v in pairs(style) do + if is_color(v) then + known_colors[v] = k + table.insert(colors, string.format([[ +--lxl_%s: %s; +]], k, get_color(v))) + end + end + for k, v in pairs(style.syntax) do + if is_color(v) then + known_colors[v] = k + table.insert(colors, string.format([[ +--lxl_%s: %s; +]], k, get_color(v))) + end + end + table.insert(draw_data, "<style>\n:root{" .. table.concat(colors) .. "}</style>") + -- Needed because we close it when we first set a clip + table.insert(draw_data, "<g>") + end + return old_begin_frame(...) +end + +local old_end_frame = renderer.end_frame +function renderer.end_frame(...) + local res = old_end_frame(...) + if screenshotting then + screenshotting = false + -- Needed to close the last clip + table.insert(draw_data, "</g>") + table.insert(draw_data, string.format("</svg>")) + core.command_view:enter("Choose a name", { + validate = function(text) return #text > 0 end, + submit = function(name) + -- Add extension if needed + name = string.gsub(name, "%.[sS][vV][gG]", "") .. ".svg" + local fp = assert( io.open(name, "wb") ) + fp:write(table.concat(draw_data, "\n")) + fp:close() + end + }) + end + return res +end + +-- Used by our renderer to round coordinates +local function rect_to_grid(x, y, w, h) + local x1, y1, x2, y2 = math.floor(x + .5), math.floor(y + .5), + math.floor(x + w + .5), math.floor(y + h + .5) + return x1, y1, x2 - x1, y2 - y1 +end + +local old_draw_rect = renderer.draw_rect +function renderer.draw_rect(x, y, width, height, color, ...) + if screenshotting then + local _x, _y, _w, _h = rect_to_grid(x, y, width, height) + local fill_color = get_fill_color(color) + table.insert(draw_data, + string.format([[<rect x="%d" y="%d" width="%d" height="%d" fill="%s" />]], + _x, _y, _w, _h, fill_color)) + end + return old_draw_rect(x, y, width, height, color, ...) +end + +local function get_font_style(font) + local path = font:get_path() + -- Only consider the first font in a fontgroup + if type(path) == "table" then path = path[1] end + local fp = assert( io.open(path, "rb") ) + local font_content = fp:read("a") + fp:close() + local name, extension = string.match(common.basename(path), "(.*)%.(.-)$") + local encoded_font = base64.encode(font_content) + -- TODO: We need a table of extensions -> mime-type + -- For now we just assume TrueType + return name, string.format([[ +<style> + @font-face{ + font-family:"%s"; + src:url(data:application/font-%s;charset=utf-8;base64,%s) format("%s"); + font-weight:normal;font-style:normal; + } +</style>]], name, extension, encoded_font, "truetype") +end + +local old_draw_text = renderer.draw_text +function renderer.draw_text(font, text, x, y, color, ...) + if screenshotting then + local font_path = font:get_path() + -- Only consider the first font in a fontgroup + if type(font_path) == "table" then font_path = font_path[1] end + if not known_fonts[font_path] then + local name, encoded_font = get_font_style(font) + known_fonts[font_path] = name + -- FIXME: We might want to keep all of those and add them all at the start, + -- before concatenating the draw_data + table.insert(draw_data, encoded_font) + end + local fill_color = get_fill_color(color) + -- Split at spaces, because multiple spaces get removed by svg renderers + for s, e in string.gmatch(text, "()%S+()") do + local partial_text = string.sub(text, s, e - 1) + partial_text = partial_text:gsub("%]%]>", "]]]]><![CDATA[>") -- escape eventual CDATA end token in the text + partial_text = partial_text:gsub("%]", "]]>]<![CDATA[") -- escape `]` because WebKit ends the CDATA with it <.< + local offset = font:get_width(string.sub(text, 1, s - 1)) + table.insert(draw_data, string.format([=[ +<text x="%.2f" y="%d" font-family="%s" font-size="%.2fpx" fill="%s"> + <![CDATA[%s]]> +</text>]=], x + offset, math.floor(y + font:get_height() * 0.8), known_fonts[font_path], + math.floor(font:get_size()), fill_color, partial_text)) + end + end + return old_draw_text(font, text, x, y, color, ...) +end + +local old_set_clip_rect = renderer.set_clip_rect +function renderer.set_clip_rect(x, y, width, height, ...) + if screenshotting then + local _x, _y, _w, _h = rect_to_grid(x, y, width, height) + current_clip = string.format("%d_%d_%d_%d", _x, _y, _w, _h) + -- Close last clip + table.insert(draw_data, "</g>") + -- Ideally we don't need this, but just use the `<g clip-path="path(...` + -- that is commented below, but it looks like each browser handles it + -- differently: + -- * Chromium considers the path as relative for some reason, and doesn't + -- seem to support `view-box` correctly. + -- * Epiphany (WebKit) has the same relative issue, but at least works with + -- `view-box`. + -- * Firefox seems to handle it correctly. + -- + -- So for now let's just use the clipPaths with their id... + if not known_clips[current_clip] then + known_clips[current_clip] = true + table.insert(draw_data, string.format([[ +<clipPath id="clip-%s"> + <rect x="%d" y="%d" width="%d" height="%d" /> +</clipPath>]], current_clip, _x, _y, _w, _h)) + end + +-- table.insert(draw_data, string.format([[ +-- <g clip-path="path('M%d %d h%d v%d h%d Z') view-box"> +-- ]], _x, _y, _w, _h, -_w)) + + table.insert(draw_data, string.format([[ +<g clip-path="url(#clip-%s)"> +]], current_clip)) + end + return old_set_clip_rect(x, y, width, height, ...) +end + +command.add(nil, { + ["screenshot:svg-screenshot"] = function() + start_screenshot = true + end +}) + +keymap.add({ + ["ctrl+f12"] = "screenshot:svg-screenshot" +}) |