aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md6
-rw-r--r--manifest.json87
-rw-r--r--plugins/colorpicker.lua56
-rw-r--r--plugins/custom_caret.lua73
-rw-r--r--plugins/keymap_export.lua265
-rw-r--r--plugins/language_rivet.lua34
-rw-r--r--plugins/minimap.lua33
-rw-r--r--plugins/search_ui.lua979
-rw-r--r--plugins/settings.lua14
-rw-r--r--plugins/svg_screenshot.lua345
10 files changed, 1766 insertions, 126 deletions
diff --git a/README.md b/README.md
index 5d7dd97..4b79303 100644
--- a/README.md
+++ b/README.md
@@ -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"
+})