Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56ef7344f0 | ||
|
|
273965ef0b | ||
|
|
597d8a42f6 | ||
|
|
b8feb4f4db | ||
|
|
3bae6fd448 | ||
|
|
a693cb645c | ||
|
|
ad7256b4fb
|
||
|
|
0af4dae0e2 | ||
|
|
b5ee18c966 | ||
|
|
85c2c3fdc8 | ||
|
|
b5317c5318 | ||
|
|
7a2214ae0e | ||
|
|
94b2b28377 | ||
|
|
409d2cd00b | ||
|
|
fceb773d96 | ||
|
|
ceea55e97d |
@@ -5,3 +5,7 @@
|
|||||||
winlib/* linguist-vendored
|
winlib/* linguist-vendored
|
||||||
src/lib/* linguist-vendored
|
src/lib/* linguist-vendored
|
||||||
resources/icons/icon.inl linguist-vendored
|
resources/icons/icon.inl linguist-vendored
|
||||||
|
|
||||||
|
# Vendored test fixtures must keep their original bytes (crlf.in tests
|
||||||
|
# CRLF handling; normalizing it would corrupt the fixture).
|
||||||
|
debian/extra/plugins/editorconfig/tests/** -text
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ on:
|
|||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
description: Release Version
|
description: Release Version
|
||||||
default: v2.1.7
|
default: v2.1.8
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -25,3 +25,6 @@ LiteXL*
|
|||||||
!resources/macos/*.py
|
!resources/macos/*.py
|
||||||
!scripts/innosetup/lite-xl@*.bmp
|
!scripts/innosetup/lite-xl@*.bmp
|
||||||
!scripts/innosetup/lite-xl-banner@*.bmp
|
!scripts/innosetup/lite-xl-banner@*.bmp
|
||||||
|
debian/.debhelper/
|
||||||
|
debian/files
|
||||||
|
debhelper-build-stamp
|
||||||
|
|||||||
@@ -1,5 +1,92 @@
|
|||||||
# Changes Log
|
# Changes Log
|
||||||
|
|
||||||
|
## [2.1.8] - 2025-06-09
|
||||||
|
|
||||||
|
This release will be the first to use [SDL3](https://wiki.libsdl.org/SDL3/FrontPage),
|
||||||
|
which should fix a bunch of issues (and might cause others).
|
||||||
|
This release also uses LTO to achieve some performance gain.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Port to SDL3
|
||||||
|
([#1756](https://github.com/lite-xl/lite-xl/pull/1756))
|
||||||
|
|
||||||
|
* Add SDL App Metadata
|
||||||
|
([#2068](https://github.com/lite-xl/lite-xl/pull/2068))
|
||||||
|
|
||||||
|
* Warn against malformed patterns and disable them
|
||||||
|
([#2029](https://github.com/lite-xl/lite-xl/pull/2029))
|
||||||
|
|
||||||
|
* Update Windows setup files
|
||||||
|
([#1988](https://github.com/lite-xl/lite-xl/pull/1988))
|
||||||
|
|
||||||
|
* build.sh: add LTO support
|
||||||
|
([#2049](https://github.com/lite-xl/lite-xl/pull/2049))
|
||||||
|
|
||||||
|
* Highlight .rockspec files
|
||||||
|
([#2080](https://github.com/lite-xl/lite-xl/pull/2080))
|
||||||
|
|
||||||
|
* Make font loading function return proper error messages
|
||||||
|
([#1919](https://github.com/lite-xl/lite-xl/pull/1919)
|
||||||
|
|
||||||
|
* Defer bitmap rendering when possible
|
||||||
|
([#1856](https://github.com/lite-xl/lite-xl/pull/1856))
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* Update CSS plugin to support more units
|
||||||
|
([#2010](https://github.com/lite-xl/lite-xl/pull/2010))
|
||||||
|
|
||||||
|
* Add __tostring method to all objects
|
||||||
|
([#1534](https://github.com/lite-xl/lite-xl/pull/1534))
|
||||||
|
|
||||||
|
* Use current char to determine col in DocView:get_x_offset_col
|
||||||
|
([#1946](https://github.com/lite-xl/lite-xl/pull/1946))
|
||||||
|
|
||||||
|
* Allow / and \ as PATHSEP on Windows when fuzzy matching files
|
||||||
|
([#1992](https://github.com/lite-xl/lite-xl/pull/1992))
|
||||||
|
|
||||||
|
* Use correct charmap and glyphmap sizes
|
||||||
|
([#1999](https://github.com/lite-xl/lite-xl/pull/1999))
|
||||||
|
|
||||||
|
* Add digit separators for C++ syntax highlighting
|
||||||
|
([#2026](https://github.com/lite-xl/lite-xl/pull/2026))
|
||||||
|
|
||||||
|
* Prevent enumerating the directory tree in system.list_dir
|
||||||
|
([#2059](https://github.com/lite-xl/lite-xl/pull/2059))
|
||||||
|
|
||||||
|
* Fixed dirmonitor excessively high CPU usage on Linux
|
||||||
|
([#2044](https://github.com/lite-xl/lite-xl/pull/2044))
|
||||||
|
|
||||||
|
* Use correct charmap and glyphmap sizes
|
||||||
|
([#1999](https://github.com/lite-xl/lite-xl/pull/1999))
|
||||||
|
|
||||||
|
* Make project folder detection more robust against unexpected errors
|
||||||
|
([b5317c5](https://github.com/lite-xl/lite-xl/commit/b5317c531837a5d4031dfdae5a52ad969735a22a))
|
||||||
|
|
||||||
|
### Other changes
|
||||||
|
|
||||||
|
* Replace memory functions with SDL equivalent
|
||||||
|
([#2067](https://github.com/lite-xl/lite-xl/pull/2067))
|
||||||
|
|
||||||
|
* Add postrelease workflow to update docs and release winget packages
|
||||||
|
([#1983](https://github.com/lite-xl/lite-xl/pull/1983))
|
||||||
|
|
||||||
|
* Move dependency resolving into source file
|
||||||
|
([#1937](https://github.com/lite-xl/lite-xl/pull/1937))
|
||||||
|
|
||||||
|
* Improve WASM support
|
||||||
|
([#1779](https://github.com/lite-xl/lite-xl/pull/1779))
|
||||||
|
|
||||||
|
* Update CI Script to fix MSYS2
|
||||||
|
([#2028](https://github.com/lite-xl/lite-xl/pull/2028))
|
||||||
|
|
||||||
|
* Fix missing includes
|
||||||
|
([#2017](https://github.com/lite-xl/lite-xl/pull/2017))
|
||||||
|
|
||||||
|
* Add manifest file to allow installing Lite XL with lpm
|
||||||
|
([#2043](https://github.com/lite-xl/lite-xl/pull/2043))
|
||||||
|
|
||||||
## [2.1.7] - 2024-12-05
|
## [2.1.7] - 2024-12-05
|
||||||
|
|
||||||
This release fixes a bug related to scaling on macOS,
|
This release fixes a bug related to scaling on macOS,
|
||||||
|
|||||||
+32
-17
@@ -56,12 +56,13 @@ end
|
|||||||
|
|
||||||
function core.set_project_dir(new_dir, change_project_fn)
|
function core.set_project_dir(new_dir, change_project_fn)
|
||||||
local chdir_ok = pcall(system.chdir, new_dir)
|
local chdir_ok = pcall(system.chdir, new_dir)
|
||||||
if chdir_ok then
|
local list_dir_ok = system.list_dir(new_dir)
|
||||||
|
if chdir_ok and list_dir_ok then
|
||||||
if change_project_fn then change_project_fn() end
|
if change_project_fn then change_project_fn() end
|
||||||
core.project_dir = common.normalize_volume(new_dir)
|
core.project_dir = common.normalize_volume(new_dir)
|
||||||
core.project_directories = {}
|
core.project_directories = {}
|
||||||
end
|
end
|
||||||
return chdir_ok
|
return chdir_ok and list_dir_ok ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
@@ -762,27 +763,41 @@ function core.init()
|
|||||||
-- Load user module, plugins and project module
|
-- Load user module, plugins and project module
|
||||||
local got_user_error, got_project_error = not core.load_user_directory()
|
local got_user_error, got_project_error = not core.load_user_directory()
|
||||||
|
|
||||||
local project_dir_abs = system.absolute_path(project_dir)
|
-- try to load a bunch of directories
|
||||||
-- We prevent set_project_dir below to effectively add and scan the directory because the
|
local try_project_dirs = {project_dir}
|
||||||
-- project module and its ignore files is not yet loaded.
|
if project_dir ~= "." then table.insert(try_project_dirs, ".") end
|
||||||
local set_project_ok = project_dir_abs and core.set_project_dir(project_dir_abs)
|
-- if cwd is not accessible for some reason (SIP on apple when launched from Finder), try home folder
|
||||||
if set_project_ok then
|
if PLATFORM == "Mac OS X" then table.insert(try_project_dirs, string.format("/Users/%s", os.getenv("USER"))) end
|
||||||
|
if PLATFORM == "Windows" then table.insert(try_project_dirs, string.format("%s\\%s", os.getenv("HOMEDRIVE"), os.getenv("HOMEPATH"))) end
|
||||||
|
-- FIXME: better POSIX detection
|
||||||
|
if os.getenv("USER") then table.insert(try_project_dirs, string.format("/home/%s", os.getenv("USER"))) end
|
||||||
|
-- discouraged since it can be blocked in sandboxed runtimes (SIP)
|
||||||
|
table.insert(try_project_dirs, os.getenv("HOMEDRIVE") and (os.getenv("HOMEDRIVE").."\\") or "/")
|
||||||
|
-- discouraged since it could be unreadable or embedded in the exe
|
||||||
|
table.insert(try_project_dirs, DATADIR)
|
||||||
|
|
||||||
|
local project_dir_abs
|
||||||
|
local function load_project_module()
|
||||||
got_project_error = not core.load_project_module()
|
got_project_error = not core.load_project_module()
|
||||||
if project_dir_explicit then
|
|
||||||
update_recents_project("add", project_dir_abs)
|
|
||||||
end
|
end
|
||||||
|
for _, p in ipairs(try_project_dirs) do
|
||||||
|
local abs_dir = system.absolute_path(p)
|
||||||
|
if abs_dir and core.set_project_dir(abs_dir, load_project_module) then
|
||||||
|
if p == project_dir and project_dir_explicit then
|
||||||
|
update_recents_project("add", abs_dir)
|
||||||
|
end
|
||||||
|
project_dir_abs = abs_dir
|
||||||
|
break
|
||||||
else
|
else
|
||||||
if not project_dir_explicit then
|
if p == project_dir and not project_dir_explicit then
|
||||||
update_recents_project("remove", project_dir)
|
update_recents_project("remove", p)
|
||||||
end
|
end
|
||||||
project_dir_abs = system.absolute_path(".")
|
end
|
||||||
if not core.set_project_dir(project_dir_abs, function()
|
end
|
||||||
got_project_error = not core.load_project_module()
|
if not project_dir_abs then
|
||||||
end) then
|
system.show_fatal_error("Error", "Please restart Lite XL with a readable project directory.")
|
||||||
system.show_fatal_error("Lite XL internal error", "cannot set project directory to cwd")
|
|
||||||
os.exit(1)
|
os.exit(1)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
-- Load core plugins after user ones to let the user override them
|
-- Load core plugins after user ones to let the user override them
|
||||||
local plugins_success, plugins_refuse_list = core.load_plugins()
|
local plugins_success, plugins_refuse_list = core.load_plugins()
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
debian/files
|
||||||
|
debian/.debhelper/
|
||||||
|
debian/*.log
|
||||||
|
debian/lite-xl/
|
||||||
|
debian/debhelper-build-stamp
|
||||||
|
debian/*.substvars
|
||||||
Vendored
+75
@@ -0,0 +1,75 @@
|
|||||||
|
lite-xl (2.1.8-7) stonking; urgency=medium
|
||||||
|
|
||||||
|
* Vendor five more plugins from lite-xl-plugins @ 31eef29:
|
||||||
|
- selectionhighlight: highlights other occurrences of the selected word
|
||||||
|
- bracketmatch: highlights the bracket matching the one under the cursor
|
||||||
|
- colorpreview: draws inline swatches for #hex/rgb()/hsl() color literals
|
||||||
|
- navigate: Go Back / Go Forward cursor history (alt+left/right)
|
||||||
|
- editorconfig: applies .editorconfig files (ships its parser and test
|
||||||
|
harness; uses the bundled trimwhitespace plugin)
|
||||||
|
|
||||||
|
-- Valentin Haudiquet <valentin.haudiquet@canonical.com> Thu, 17 Sep 2026 11:30:00 +0200
|
||||||
|
|
||||||
|
lite-xl (2.1.8-6) stonking; urgency=medium
|
||||||
|
|
||||||
|
* Vendor ephemeral_tabs (lite-xl-plugins @ 31eef29): files opened from the
|
||||||
|
treeview with a single click open in a preview tab (titled "~ name ~")
|
||||||
|
that is reused for the next file and becomes a permanent tab as soon as
|
||||||
|
it is edited, double-clicked or dragged - matching VSCode's preview
|
||||||
|
editor behavior. Disable with config.plugins.ephemeral_tabs = false.
|
||||||
|
|
||||||
|
-- Valentin Haudiquet <valentin.haudiquet@canonical.com> Thu, 17 Sep 2026 10:45:00 +0200
|
||||||
|
|
||||||
|
lite-xl (2.1.8-5) stonking; urgency=medium
|
||||||
|
|
||||||
|
* Vendor gitpanel 0.3.0 (own plugin, lite-xl-gitpanel @ 0731a63): a git
|
||||||
|
panel integrated into the treeview pane - uncommitted changes with
|
||||||
|
diff stats, staging, commits and history - toggled from a git-branch
|
||||||
|
button in the treeview toolbar or the status bar branch indicator.
|
||||||
|
History is pinned to the bottom half of the panel; clicking a commit
|
||||||
|
opens its patch with syntax highlighting via the already bundled
|
||||||
|
language_diff.
|
||||||
|
|
||||||
|
-- Valentin Haudiquet <valentin.haudiquet@canonical.com> Wed, 17 Sep 2026 01:55:00 +0200
|
||||||
|
|
||||||
|
lite-xl (2.1.8-4) stonking; urgency=medium
|
||||||
|
|
||||||
|
* Bundle a curated set of community plugins (vendored, pinned in
|
||||||
|
debian/extra/README.source):
|
||||||
|
- gitstatus, gitopen, fontconfig, open_ext, select_colorscheme,
|
||||||
|
smoothcaret, nerdicons (+ Symbols Nerd Font Mono, font path patched
|
||||||
|
to DATADIR)
|
||||||
|
- lite-xl-lsp (full LSP client; activates when a language server is
|
||||||
|
configured) with the lite-xl-widgets library it requires
|
||||||
|
- 104 additional language syntax plugins (none duplicate the nine
|
||||||
|
already bundled with core)
|
||||||
|
* settings GUI plugin left out: its hard dependency on a widget that is
|
||||||
|
missing from lite-xl-widgets@master makes it fail to load upstream.
|
||||||
|
|
||||||
|
-- Valentin Haudiquet <valentin.haudiquet@canonical.com> Wed, 16 Sep 2026 22:10:00 +0200
|
||||||
|
|
||||||
|
lite-xl (2.1.8-3) stonking; urgency=medium
|
||||||
|
|
||||||
|
* Ship a VSCode-style dark theme and out-of-the-box config:
|
||||||
|
- first-run template writes a tuned default init.lua (quilt patch)
|
||||||
|
- theme module in /usr/share/lite-xl/colors, default config in /etc/skel
|
||||||
|
- default config uses JetBrains Mono (new fonts-jetbrains-mono dependency)
|
||||||
|
* desktop: launch with SDL software rendering and LITE_SCALE=1.5 so text is
|
||||||
|
sharp on fractional-scaled Wayland panels and avoids panfrost artifacts.
|
||||||
|
* vendored nerdicons ships with the tuned icon rendering (whole-pixel size,
|
||||||
|
hinting off) instead of the pristine upstream copy.
|
||||||
|
* default config keeps the status bar always visible.
|
||||||
|
|
||||||
|
-- Valentin Haudiquet <valentin.haudiquet@canonical.com> Wed, 16 Sep 2026 21:50:00 +0200
|
||||||
|
|
||||||
|
lite-xl (2.1.8-2) stonking; urgency=medium
|
||||||
|
|
||||||
|
* Drop the ":native" qualifiers on the meson/ninja-build Build-Depends.
|
||||||
|
|
||||||
|
-- Valentin Haudiquet <valentin.haudiquet@canonical.com> Wed, 16 Sep 2026 16:40:00 +0200
|
||||||
|
|
||||||
|
lite-xl (2.1.8-1) stonking; urgency=medium
|
||||||
|
|
||||||
|
* Initial release.
|
||||||
|
|
||||||
|
-- Valentin Haudiquet <valentin.haudiquet@canonical.com> Wed, 16 Sep 2026 13:56:53 +0200
|
||||||
Vendored
+22
@@ -0,0 +1,22 @@
|
|||||||
|
Source: lite-xl
|
||||||
|
Section: utils
|
||||||
|
Priority: optional
|
||||||
|
Maintainer: Valentin Haudiquet <valentin.haudiquet@canonical.com>
|
||||||
|
Rules-Requires-Root: no
|
||||||
|
Standards-Version: 4.7.2
|
||||||
|
Build-Depends: debhelper-compat (= 13),
|
||||||
|
meson,
|
||||||
|
ninja-build,
|
||||||
|
libsdl3-dev,
|
||||||
|
libfreetype-dev,
|
||||||
|
libpcre2-dev,
|
||||||
|
liblua5.4-dev
|
||||||
|
Homepage: https://lite-xl.com/
|
||||||
|
|
||||||
|
Package: lite-xl
|
||||||
|
Architecture: any
|
||||||
|
Depends: ${shlibs:Depends}, ${misc:Depends}, fonts-jetbrains-mono
|
||||||
|
Description: A lightweight text editor written in Lua
|
||||||
|
A lightweight, fast and extensible text editor written in Lua, designed to be
|
||||||
|
highly customizable and to have a small footprint. It is written in C and Lua
|
||||||
|
and uses the SDL library for cross-platform rendering.
|
||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||||
|
Upstream-Name: lite-xl
|
||||||
|
Source: https://lite-xl.com/
|
||||||
|
|
||||||
|
Files: *
|
||||||
|
Copyright: 2026 Valentin Haudiquet <valentin.haudiquet@canonical.com>
|
||||||
|
License: MIT
|
||||||
|
The package is distributed under the terms of the MIT license. The full license text is available at <https://spdx.org/licenses/MIT.html>.
|
||||||
|
|
||||||
|
License: MIT
|
||||||
|
The package is distributed under the terms of the MIT license. The full license text is available at <https://spdx.org/licenses/MIT.html>.
|
||||||
Vendored
+27
@@ -0,0 +1,27 @@
|
|||||||
|
Vendored from upstream, pinned by commit:
|
||||||
|
|
||||||
|
- lite-xl-plugins https://github.com/lite-xl/lite-xl-plugins @ 31eef29 (master, 2026-09)
|
||||||
|
plugins/: ephemeral_tabs (VSCode-style preview tabs), selectionhighlight,
|
||||||
|
bracketmatch, colorpreview, navigate, editorconfig (whole directory,
|
||||||
|
incl. its test harness), gitstatus, gitopen, fontconfig, open_ext,
|
||||||
|
select_colorscheme, smoothcaret, nerdicons, language_* (all 104; none
|
||||||
|
overlap the ones bundled in the core data/ directory)
|
||||||
|
- lite-xl-gitpanel (own plugin, source tree lite-xl-gitpanel) @ 0731a63 (main, 2026-09)
|
||||||
|
plugins/: gitpanel -- git panel integrated into the treeview pane
|
||||||
|
(changes, staging, commits, history) with a toolbar toggle button;
|
||||||
|
uses the bundled language_diff for commit diff highlighting
|
||||||
|
|
||||||
|
- lite-xl-lsp https://github.com/lite-xl/lite-xl-lsp @ d1432ae (master, 2026-09)
|
||||||
|
vendored whole as plugins/lsp/ (screenshots removed)
|
||||||
|
- lite-xl-widgets https://github.com/lite-xl/lite-xl-widgets @ dcf1c8c
|
||||||
|
NOT vendored: only the settings plugin needs it, and settings@master
|
||||||
|
requires libraries.widget.fonts which does not exist in the widgets
|
||||||
|
repository at any revision, so settings cannot load. Revisit when
|
||||||
|
upstream publishes the missing widget.
|
||||||
|
- nerdicons font mapping: plugins/font_symbols_nerdfont_mono_regular.lua
|
||||||
|
shipped as usr/share/lite-xl/libraries/font_symbols_nerdfont_mono_regular.lua
|
||||||
|
with the font path patched from USERDIR to DATADIR, plus the
|
||||||
|
Symbols Nerd Font Mono TTF (SIL OFL 1.1) next to it.
|
||||||
|
|
||||||
|
Refresh by re-cloning the sources at a new commit and re-applying the single
|
||||||
|
sed patch to the font mapping path.
|
||||||
Binary file not shown.
Vendored
+80
@@ -0,0 +1,80 @@
|
|||||||
|
-- Lite XL default configuration (shipped via /etc/skel or the first-run
|
||||||
|
-- template). This file is yours to edit; it is reloaded automatically on save.
|
||||||
|
-- It gives the editor a VSCode-style dark look out of the box.
|
||||||
|
|
||||||
|
local core = require "core"
|
||||||
|
local common = require "core.common"
|
||||||
|
local config = require "core.config"
|
||||||
|
local style = require "core.style"
|
||||||
|
|
||||||
|
------------------------------ Appearance -------------------------------------
|
||||||
|
-- VSCode "Dark Modern" theme (shipped in /usr/share/lite-xl/colors).
|
||||||
|
core.reload_module("colors.vscode_dark")
|
||||||
|
|
||||||
|
-- Sharp text: grayscale antialiasing with light autohinting renders crisper
|
||||||
|
-- than the defaults on high-DPI panels.
|
||||||
|
local font_opts = { antialiasing = "grayscale", hinting = "slight", smoothing = true }
|
||||||
|
|
||||||
|
-- Bump these for a bigger/smaller UI; ctrl+wheel also zooms live.
|
||||||
|
local UI_FONT_SIZE = 15
|
||||||
|
local CODE_FONT_SIZE = 16
|
||||||
|
|
||||||
|
style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf",
|
||||||
|
UI_FONT_SIZE * SCALE, font_opts)
|
||||||
|
style.big_font = style.font:copy(46 * SCALE)
|
||||||
|
style.code_font = renderer.font.load(
|
||||||
|
"/usr/share/fonts/truetype/jetbrains-mono/JetBrainsMono-Regular.ttf",
|
||||||
|
CODE_FONT_SIZE * SCALE, font_opts)
|
||||||
|
|
||||||
|
------------------------------ Behavior ---------------------------------------
|
||||||
|
|
||||||
|
config.line_height = 1.3 -- airy lines, like VSCode's default
|
||||||
|
config.indent_size = 4
|
||||||
|
config.plugins.lineguide = true -- indent guides, like VSCode
|
||||||
|
|
||||||
|
------------------------------ Treeview sidebar -------------------------------
|
||||||
|
-- Tighter rows and VSCode-style hover/selection highlight. Method overrides so
|
||||||
|
-- the shared style.padding used by the editor margins is untouched.
|
||||||
|
|
||||||
|
local TreeView = require "plugins.treeview"
|
||||||
|
|
||||||
|
local tv_row_pad = common.round(3 * SCALE)
|
||||||
|
function TreeView:get_item_height()
|
||||||
|
return style.font:get_height() + tv_row_pad
|
||||||
|
end
|
||||||
|
|
||||||
|
local tv_base_indent = common.round(8 * SCALE)
|
||||||
|
local tv_level_indent = common.round(10 * SCALE)
|
||||||
|
function TreeView:draw_item(item, active, hovered, x, y, w, h)
|
||||||
|
self:draw_item_background(item, active, hovered, x, y, w, h)
|
||||||
|
x = x + tv_base_indent + item.depth * tv_level_indent
|
||||||
|
x = x + self:draw_item_chevron(item, active, hovered, x, y, w, h)
|
||||||
|
self:draw_item_body(item, active, hovered, x, y, w, h)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TreeView:get_item_text(item, active, hovered)
|
||||||
|
local color = (active or hovered)
|
||||||
|
and (style.treeview_text_active or style.accent)
|
||||||
|
or (style.treeview_text or style.text)
|
||||||
|
return item.name, style.font, color
|
||||||
|
end
|
||||||
|
|
||||||
|
function TreeView:draw_item_background(item, active, hovered, x, y, w, h)
|
||||||
|
if hovered then
|
||||||
|
local c = { table.unpack(style.treeview_hover_bg or style.line_highlight) }
|
||||||
|
c[4] = 160
|
||||||
|
renderer.draw_rect(x, y, w, h, c)
|
||||||
|
elseif active then
|
||||||
|
renderer.draw_rect(x, y, w, h, style.treeview_active_bg or style.line_highlight)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
------------------------------ Status bar -------------------------------------
|
||||||
|
-- Keep the status bar always visible: show it at startup and turn the
|
||||||
|
-- status-bar:hide / status-bar:toggle commands into no-ops so it cannot
|
||||||
|
-- be dismissed (from the command palette or by any plugin).
|
||||||
|
|
||||||
|
core.status_view:show()
|
||||||
|
local StatusView = require "core.statusview"
|
||||||
|
function StatusView:toggle() self:show() end
|
||||||
|
function StatusView:hide() self:show() end
|
||||||
File diff suppressed because it is too large
Load Diff
+163
@@ -0,0 +1,163 @@
|
|||||||
|
--
|
||||||
|
-- Button Widget.
|
||||||
|
-- @copyright Jefferson Gonzalez
|
||||||
|
-- @license MIT
|
||||||
|
--
|
||||||
|
|
||||||
|
local style = require "core.style"
|
||||||
|
local Widget = require "libraries.widget"
|
||||||
|
|
||||||
|
---@class widget.button.icon
|
||||||
|
---@field public code string | nil
|
||||||
|
---@field public color renderer.color | nil
|
||||||
|
---@field public hover_color renderer.color | nil
|
||||||
|
local ButtonIcon = {}
|
||||||
|
|
||||||
|
---@class widget.button : widget
|
||||||
|
---@overload fun(parent:widget?, label:string?):widget.button
|
||||||
|
---@field public padding widget.position
|
||||||
|
---@field public icon widget.button.icon
|
||||||
|
---@field public expanded boolean
|
||||||
|
local Button = Widget:extend()
|
||||||
|
|
||||||
|
---Constructor
|
||||||
|
---@param parent widget
|
||||||
|
---@param label? string
|
||||||
|
function Button:new(parent, label)
|
||||||
|
Button.super.new(self, parent)
|
||||||
|
|
||||||
|
self.type_name = "widget.button"
|
||||||
|
|
||||||
|
self.icon = {
|
||||||
|
code = nil, color = nil, hover_color = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.padding = {
|
||||||
|
x = style.padding.x,
|
||||||
|
y = style.padding.y
|
||||||
|
}
|
||||||
|
|
||||||
|
self.expanded = false
|
||||||
|
|
||||||
|
self:set_label(label or "")
|
||||||
|
end
|
||||||
|
|
||||||
|
---When set to true the button width will be the same as parent
|
||||||
|
---@param expand? boolean | nil
|
||||||
|
function Button:toggle_expand(expand)
|
||||||
|
if type(expand) == "boolean" then
|
||||||
|
self.expanded = expand
|
||||||
|
else
|
||||||
|
self.expanded = not self.expanded
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set the icon drawn alongside the button text.
|
||||||
|
---@param code? string
|
||||||
|
---@param color? renderer.color
|
||||||
|
---@param hover_color? renderer.color
|
||||||
|
function Button:set_icon(code, color, hover_color)
|
||||||
|
self.icon.code = code
|
||||||
|
self.icon.color = color
|
||||||
|
self.icon.hover_color = hover_color
|
||||||
|
|
||||||
|
self:set_label(self.label)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set the button text and recalculates the widget size.
|
||||||
|
---@param text string
|
||||||
|
function Button:set_label(text)
|
||||||
|
Button.super.set_label(self, text)
|
||||||
|
|
||||||
|
local font = self:get_font()
|
||||||
|
local border = self.border.width * 2
|
||||||
|
|
||||||
|
if self.expanded and self.parent then
|
||||||
|
self.size.x = self.parent.size.x - self.position.rx - border
|
||||||
|
else
|
||||||
|
self.size.x = font:get_width(self.label) + (self.padding.x * 2) - border
|
||||||
|
end
|
||||||
|
|
||||||
|
self.size.y = font:get_height() + (self.padding.y * 2) - border
|
||||||
|
|
||||||
|
if self.icon.code then
|
||||||
|
local icon_w = style.icon_font:get_width(self.icon.code)
|
||||||
|
|
||||||
|
if self.label ~= "" then
|
||||||
|
icon_w = icon_w + (self.padding.x / 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
local icon_h = style.icon_font:get_height() + (self.padding.y * 2) - border
|
||||||
|
|
||||||
|
self.size.x = self.size.x + icon_w
|
||||||
|
self.size.y = math.max(self.size.y, icon_h)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Button:on_mouse_enter(...)
|
||||||
|
Button.super.on_mouse_enter(self, ...)
|
||||||
|
self.hover_text = style.accent
|
||||||
|
self.hover_back = style.line_highlight
|
||||||
|
end
|
||||||
|
|
||||||
|
function Button:on_mouse_leave(...)
|
||||||
|
Button.super.on_mouse_leave(self, ...)
|
||||||
|
self.hover_text = nil
|
||||||
|
self.hover_back = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function Button:on_scale_change(new_scale, prev_scale)
|
||||||
|
Button.super.on_scale_change(self, new_scale, prev_scale)
|
||||||
|
self.padding.x = self.padding.x * (new_scale / prev_scale)
|
||||||
|
self.padding.y = self.padding.y * (new_scale / prev_scale)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Button:update()
|
||||||
|
if not Button.super.update(self) then return false end
|
||||||
|
|
||||||
|
-- update size
|
||||||
|
self:set_label(self.label)
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function Button:draw()
|
||||||
|
self.background_color = self.hover_back or style.background
|
||||||
|
|
||||||
|
if not Button.super.draw(self) then return false end
|
||||||
|
|
||||||
|
local font = self:get_font()
|
||||||
|
|
||||||
|
local offsetx = self.position.x + self.padding.x
|
||||||
|
local offsety = self.position.y
|
||||||
|
local h = self:get_height()
|
||||||
|
local ih, th = style.icon_font:get_height(), font:get_height()
|
||||||
|
|
||||||
|
if self.icon.code then
|
||||||
|
local normal = self.icon.color or style.text
|
||||||
|
local hover = self.icon.hover_color or style.accent
|
||||||
|
renderer.draw_text(
|
||||||
|
style.icon_font,
|
||||||
|
self.icon.code,
|
||||||
|
offsetx,
|
||||||
|
th > ih and (offsety + (h / 2)) - (ih/2) or (offsety + self.padding.y),
|
||||||
|
self.hover_text and hover or normal
|
||||||
|
)
|
||||||
|
offsetx = offsetx + style.icon_font:get_width(self.icon.code) + (style.padding.x / 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.label ~= "" then
|
||||||
|
renderer.draw_text(
|
||||||
|
font,
|
||||||
|
self.label,
|
||||||
|
offsetx,
|
||||||
|
ih > th and (offsety + (h / 2)) - (th/2) or (offsety + self.padding.y),
|
||||||
|
self.hover_text or self.foreground_color or style.text
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return Button
|
||||||
+142
@@ -0,0 +1,142 @@
|
|||||||
|
--
|
||||||
|
-- CheckBox Widget.
|
||||||
|
-- @copyright Jefferson Gonzalez
|
||||||
|
-- @license MIT
|
||||||
|
--
|
||||||
|
|
||||||
|
local style = require "core.style"
|
||||||
|
local Widget = require "libraries.widget"
|
||||||
|
|
||||||
|
---@class widget.checkbox : widget
|
||||||
|
---@overload fun(parent?:widget, label?:string):widget.checkbox
|
||||||
|
---@field private checked boolean
|
||||||
|
local CheckBox = Widget:extend()
|
||||||
|
|
||||||
|
---Constructor
|
||||||
|
---@param parent widget
|
||||||
|
---@param label string
|
||||||
|
function CheckBox:new(parent, label)
|
||||||
|
CheckBox.super.new(self, parent)
|
||||||
|
self.type_name = "widget.checkbox"
|
||||||
|
self.checked = false
|
||||||
|
self:set_label(label or "")
|
||||||
|
self.animating = false
|
||||||
|
self.animating_color = style.caret
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set the checkbox label and recalculates the widget size.
|
||||||
|
---@param text string
|
||||||
|
function CheckBox:set_label(text)
|
||||||
|
CheckBox.super.set_label(self, text)
|
||||||
|
|
||||||
|
local _, _, bw, _ = self:get_box_rect()
|
||||||
|
|
||||||
|
local font = self:get_font()
|
||||||
|
|
||||||
|
self.size.x = font:get_width(self.label) + bw + (style.padding.x / 2)
|
||||||
|
self.size.y = font:get_height()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Change the status of the checkbox.
|
||||||
|
---@param checked boolean
|
||||||
|
function CheckBox:set_checked(checked)
|
||||||
|
self.checked = checked
|
||||||
|
self:on_change(self.checked)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the status of the checkbox.
|
||||||
|
---@return boolean
|
||||||
|
function CheckBox:is_checked()
|
||||||
|
return self.checked
|
||||||
|
end
|
||||||
|
|
||||||
|
---Called when the checkbox is (un)checked.
|
||||||
|
---@param checked boolean
|
||||||
|
function CheckBox:on_checked(checked) end
|
||||||
|
|
||||||
|
function CheckBox:on_mouse_enter(...)
|
||||||
|
CheckBox.super.on_mouse_enter(self, ...)
|
||||||
|
self.hover_text = style.accent
|
||||||
|
self.hover_back = style.dim
|
||||||
|
end
|
||||||
|
|
||||||
|
function CheckBox:on_mouse_leave(...)
|
||||||
|
CheckBox.super.on_mouse_leave(self, ...)
|
||||||
|
self.hover_text = nil
|
||||||
|
self.hover_back = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function CheckBox:on_click()
|
||||||
|
self.checked = not self.checked
|
||||||
|
self:on_checked(self.checked)
|
||||||
|
self:on_change(self.checked)
|
||||||
|
|
||||||
|
self.animating = true
|
||||||
|
self.animating_color = {table.unpack(style.caret)}
|
||||||
|
local target_color = {table.unpack(style.caret)}
|
||||||
|
|
||||||
|
if self.checked then
|
||||||
|
self.animating_color[4] = 0
|
||||||
|
target_color[4] = 255
|
||||||
|
else
|
||||||
|
self.animating_color[4] = 255
|
||||||
|
target_color[4] = 0
|
||||||
|
end
|
||||||
|
self:animate(self.animating_color, {table.unpack(target_color)}, {
|
||||||
|
on_complete = function()
|
||||||
|
self.animating = false
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function CheckBox:get_box_rect()
|
||||||
|
local size = 1.6
|
||||||
|
local font = self:get_font()
|
||||||
|
local fh = font:get_height() / size
|
||||||
|
return
|
||||||
|
self.position.x,
|
||||||
|
self.position.y + (fh / (size * 2)),
|
||||||
|
font:get_width("x") + 4,
|
||||||
|
fh
|
||||||
|
end
|
||||||
|
|
||||||
|
function CheckBox:update()
|
||||||
|
if not CheckBox.super.update(self) then return false end
|
||||||
|
|
||||||
|
-- update size
|
||||||
|
self:set_label(self.label)
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function CheckBox:draw()
|
||||||
|
if not self:is_visible() then return false end
|
||||||
|
|
||||||
|
local bx, by, bw, bh = self:get_box_rect()
|
||||||
|
|
||||||
|
self:draw_border(bx, by, bw, bh)
|
||||||
|
|
||||||
|
renderer.draw_rect(
|
||||||
|
bx, by, bw, bh,
|
||||||
|
self.hover_back or self.background_color or style.background
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.animating then
|
||||||
|
renderer.draw_rect(bx + 2, by + 2, bw-4, bh-4, self.animating_color)
|
||||||
|
elseif self.checked then
|
||||||
|
renderer.draw_rect(bx + 2, by + 2, bw-4, bh-4, style.caret)
|
||||||
|
end
|
||||||
|
|
||||||
|
renderer.draw_text(
|
||||||
|
self:get_font(),
|
||||||
|
self.label,
|
||||||
|
self.position.x + bw + (style.padding.x / 2),
|
||||||
|
self.position.y,
|
||||||
|
self.hover_text or self.foreground_color or style.text
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return CheckBox
|
||||||
+745
@@ -0,0 +1,745 @@
|
|||||||
|
--
|
||||||
|
-- Color Picker Widget
|
||||||
|
-- Note: HSV and HSL conversion functions adapted from:
|
||||||
|
-- https://github.com/EmmanuelOga/columns/blob/master/utils/color.lua
|
||||||
|
-- which in turn was ported from:
|
||||||
|
-- http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
|
||||||
|
--
|
||||||
|
local core = require "core"
|
||||||
|
local style = require "core.style"
|
||||||
|
local common = require "core.common"
|
||||||
|
local Widget = require "libraries.widget"
|
||||||
|
local TextBox = require "libraries.widget.textbox"
|
||||||
|
|
||||||
|
---@alias widget.colorpicker.colorrange renderer.color[]
|
||||||
|
|
||||||
|
---The numerical portion of a color range on the hue selection bar.
|
||||||
|
---@type number
|
||||||
|
local HUE_COLOR_SEGMENT = 100 / 6
|
||||||
|
|
||||||
|
---Hue color ranges in the order rendered on the hue bar.
|
||||||
|
---@type widget.colorpicker.colorrange[]
|
||||||
|
local HUE_COLOR_RANGES = {
|
||||||
|
-- red -> yellow
|
||||||
|
{ {255, 0, 0, 255}, {255, 255, 0, 255} },
|
||||||
|
-- yellow -> green
|
||||||
|
{ {255, 255, 0, 255}, {0, 255, 0, 255} },
|
||||||
|
-- green -> cyan
|
||||||
|
{ {0, 255 ,0, 255}, {0, 255, 255, 255} },
|
||||||
|
-- cyan -> blue
|
||||||
|
{ {0, 255, 255, 255}, {0, 0, 255, 255} },
|
||||||
|
-- blue -> purple
|
||||||
|
{ {0, 0, 255, 255}, {255, 0, 255, 255} },
|
||||||
|
-- purple -> red
|
||||||
|
{ {255, 0, 255, 255}, {255, 0, 0, 255} }
|
||||||
|
}
|
||||||
|
|
||||||
|
---@type renderer.color
|
||||||
|
local COLOR_BLACK = {0, 0, 0, 255}
|
||||||
|
|
||||||
|
---@type renderer.color
|
||||||
|
local COLOR_WHITE = {255, 255, 255, 255}
|
||||||
|
|
||||||
|
---@class widget.colorpicker : widget
|
||||||
|
---@overload fun(parent:widget?, color?:renderer.color|string):widget.colorpicker
|
||||||
|
---@field hue_color renderer.color
|
||||||
|
---@field saturation_color renderer.color
|
||||||
|
---@field brightness_color renderer.color
|
||||||
|
---@field hue_pos number
|
||||||
|
---@field saturation_pos number
|
||||||
|
---@field brightness_pos number
|
||||||
|
---@field alpha number
|
||||||
|
---@field hue_mouse_down boolean
|
||||||
|
---@field saturation_mouse_down boolean
|
||||||
|
---@field brightness_mouse_down boolean
|
||||||
|
---@field html_notation widget.textbox
|
||||||
|
---@field rgba_notation widget.textbox
|
||||||
|
local ColorPicker = Widget:extend()
|
||||||
|
|
||||||
|
---Constructor
|
||||||
|
---@param parent widget
|
||||||
|
---@param color? renderer.color | string
|
||||||
|
function ColorPicker:new(parent, color)
|
||||||
|
ColorPicker.super.new(self, parent, false)
|
||||||
|
|
||||||
|
self.type_name = "widget.colorpicker"
|
||||||
|
|
||||||
|
self.hue_pos = 0
|
||||||
|
self.saturation_pos = 100
|
||||||
|
self.brightness_pos = 100
|
||||||
|
self.alpha = 255
|
||||||
|
|
||||||
|
self.hue_color = COLOR_BLACK
|
||||||
|
self.saturation_color = COLOR_BLACK
|
||||||
|
self.brightness_color = COLOR_BLACK
|
||||||
|
|
||||||
|
self.hue_mouse_down = false;
|
||||||
|
self.saturation_mouse_down = false
|
||||||
|
self.brightness_mouse_down = false
|
||||||
|
|
||||||
|
self.selector = { x = 0, y = 0, w = 0, h = 0 }
|
||||||
|
|
||||||
|
self:set_border_width(0)
|
||||||
|
|
||||||
|
local this = self
|
||||||
|
self.html_notation = TextBox(self, "#FF0000")
|
||||||
|
self.rgba_notation = TextBox(self, "rgba(255,0,0,1)")
|
||||||
|
self.html_updating = false
|
||||||
|
self.rgba_updating = false
|
||||||
|
|
||||||
|
function self.html_notation:on_change(value)
|
||||||
|
if
|
||||||
|
not this.hue_mouse_down
|
||||||
|
and
|
||||||
|
not this.saturation_mouse_down
|
||||||
|
and
|
||||||
|
not this.brightness_mouse_down
|
||||||
|
and
|
||||||
|
not this.html_updating
|
||||||
|
then
|
||||||
|
this:set_color(value, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function self.rgba_notation:on_change(value)
|
||||||
|
if
|
||||||
|
not this.hue_mouse_down
|
||||||
|
and
|
||||||
|
not this.saturation_mouse_down
|
||||||
|
and
|
||||||
|
not this.brightness_mouse_down
|
||||||
|
and
|
||||||
|
not this.rgba_updating
|
||||||
|
then
|
||||||
|
this:set_color(value, false, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self:set_color(color or {255, 0, 0, 255})
|
||||||
|
|
||||||
|
-- set initial child positions and size
|
||||||
|
self:update_size()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Converts an RGB color value to HSL. Conversion formula
|
||||||
|
---adapted from http://en.wikipedia.org/wiki/HSL_color_space.
|
||||||
|
---Assumes r, g, and b are contained in the set [0, 255] and
|
||||||
|
---returns h, s, and l in the set [0, 1].
|
||||||
|
---@param rgba renderer.color
|
||||||
|
---@return table hsla
|
||||||
|
function ColorPicker.rgb_to_hsl(rgba)
|
||||||
|
local r, g, b, a = rgba[1], rgba[2], rgba[3], rgba[4]
|
||||||
|
r, g, b = r / 255, g / 255, b / 255
|
||||||
|
|
||||||
|
local max, min = math.max(r, g, b), math.min(r, g, b)
|
||||||
|
local h, s, l
|
||||||
|
|
||||||
|
l = (max + min) / 2
|
||||||
|
|
||||||
|
if max == min then
|
||||||
|
h, s = 0, 0 -- achromatic
|
||||||
|
else
|
||||||
|
local d = max - min
|
||||||
|
if l > 0.5 then s = d / (2 - max - min) else s = d / (max + min) end
|
||||||
|
if max == r then
|
||||||
|
h = (g - b) / d
|
||||||
|
if g < b then h = h + 6 end
|
||||||
|
elseif max == g then h = (b - r) / d + 2
|
||||||
|
elseif max == b then h = (r - g) / d + 4
|
||||||
|
end
|
||||||
|
h = h / 6
|
||||||
|
end
|
||||||
|
|
||||||
|
return {h, s, l, a and a/255 or 1}
|
||||||
|
end
|
||||||
|
|
||||||
|
---Converts an HSL color value to RGB. Conversion formula
|
||||||
|
---adapted from http://en.wikipedia.org/wiki/HSL_color_space.
|
||||||
|
---Assumes h, s, and l are contained in the set [0, 1] and
|
||||||
|
---returns r, g, and b in the set [0, 255].
|
||||||
|
---@param h number The hue
|
||||||
|
---@param s number The saturation
|
||||||
|
---@param l number The lightness
|
||||||
|
---@param a number The alpha
|
||||||
|
---@return renderer.color rgba
|
||||||
|
function ColorPicker.hsl_to_rgb(h, s, l, a)
|
||||||
|
local r, g, b
|
||||||
|
|
||||||
|
if s == 0 then
|
||||||
|
r, g, b = l, l, l -- achromatic
|
||||||
|
else
|
||||||
|
local function hue2rgb(p, q, t)
|
||||||
|
if t < 0 then t = t + 1 end
|
||||||
|
if t > 1 then t = t - 1 end
|
||||||
|
if t < 1/6 then return p + (q - p) * 6 * t end
|
||||||
|
if t < 1/2 then return q end
|
||||||
|
if t < 2/3 then return p + (q - p) * (2/3 - t) * 6 end
|
||||||
|
return p
|
||||||
|
end
|
||||||
|
|
||||||
|
local q
|
||||||
|
if l < 0.5 then q = l * (1 + s) else q = l + s - l * s end
|
||||||
|
local p = 2 * l - q
|
||||||
|
|
||||||
|
r = hue2rgb(p, q, h + 1/3)
|
||||||
|
g = hue2rgb(p, q, h)
|
||||||
|
b = hue2rgb(p, q, h - 1/3)
|
||||||
|
end
|
||||||
|
|
||||||
|
return {r * 255, g * 255, b * 255, a * 255}
|
||||||
|
end
|
||||||
|
|
||||||
|
---Converts an RGB color value to HSV. Conversion formula
|
||||||
|
---adapted from http://en.wikipedia.org/wiki/HSV_color_space.
|
||||||
|
---Assumes r, g, and b are contained in the set [0, 255] and
|
||||||
|
---returns h, s, and v in the set [0, 1].
|
||||||
|
---@param rgba renderer.color
|
||||||
|
---@return table hsva The HSV representation
|
||||||
|
function ColorPicker.rgb_to_hsv(rgba)
|
||||||
|
local r, g, b, a = rgba[1], rgba[2], rgba[3], rgba[4]
|
||||||
|
r, g, b, a = r / 255, g / 255, b / 255, a / 255
|
||||||
|
local max, min = math.max(r, g, b), math.min(r, g, b)
|
||||||
|
local h, s, v
|
||||||
|
v = max
|
||||||
|
|
||||||
|
local d = max - min
|
||||||
|
if max == 0 then s = 0 else s = d / max end
|
||||||
|
|
||||||
|
if max == min then
|
||||||
|
h = 0 -- achromatic
|
||||||
|
else
|
||||||
|
if max == r then
|
||||||
|
h = (g - b) / d
|
||||||
|
if g < b then h = h + 6 end
|
||||||
|
elseif max == g then h = (b - r) / d + 2
|
||||||
|
elseif max == b then h = (r - g) / d + 4
|
||||||
|
end
|
||||||
|
h = h / 6
|
||||||
|
end
|
||||||
|
|
||||||
|
return {h, s, v, a/255}
|
||||||
|
end
|
||||||
|
|
||||||
|
---Converts an HSV color value to RGB. Conversion formula
|
||||||
|
---adapted from http://en.wikipedia.org/wiki/HSV_color_space.
|
||||||
|
---Assumes h, s, and v are contained in the set [0, 1] and
|
||||||
|
---returns r, g, and b in the set [0, 255].
|
||||||
|
---@param h number The hue
|
||||||
|
---@param s number The saturation
|
||||||
|
---@param v number The brightness
|
||||||
|
---@param a number The alpha
|
||||||
|
---@return renderer.color rgba The RGB representation
|
||||||
|
function ColorPicker.hsv_to_rgb(h, s, v, a)
|
||||||
|
local r, g, b
|
||||||
|
|
||||||
|
local i = math.floor(h * 6);
|
||||||
|
local f = h * 6 - i;
|
||||||
|
local p = v * (1 - s);
|
||||||
|
local q = v * (1 - f * s);
|
||||||
|
local t = v * (1 - (1 - f) * s);
|
||||||
|
|
||||||
|
i = i % 6
|
||||||
|
|
||||||
|
if i == 0 then r, g, b = v, t, p
|
||||||
|
elseif i == 1 then r, g, b = q, v, p
|
||||||
|
elseif i == 2 then r, g, b = p, v, t
|
||||||
|
elseif i == 3 then r, g, b = p, q, v
|
||||||
|
elseif i == 4 then r, g, b = t, p, v
|
||||||
|
elseif i == 5 then r, g, b = v, p, q
|
||||||
|
end
|
||||||
|
|
||||||
|
return {math.ceil(r * 255), math.ceil(g * 255), math.ceil(b * 255), math.ceil(a * 255)}
|
||||||
|
end
|
||||||
|
|
||||||
|
---Converts a css format color string into a renderer.color if possible,
|
||||||
|
---if conversion fails returns nil. Adapted from colorpreview plugin.
|
||||||
|
---@param color string
|
||||||
|
---@return renderer.color? color
|
||||||
|
function ColorPicker.color_from_string(color)
|
||||||
|
local s, e, r, g, b, a, base, nibbles;
|
||||||
|
|
||||||
|
s, e, r, g, b, a = color:find("#(%x%x)(%x%x)(%x%x)(%x?%x?)")
|
||||||
|
|
||||||
|
if s then
|
||||||
|
base = 16
|
||||||
|
else
|
||||||
|
s, e, r, g, b, a = color:find("#(%x)(%x)(%x)")
|
||||||
|
|
||||||
|
if s then
|
||||||
|
base = 16
|
||||||
|
nibbles = true
|
||||||
|
else
|
||||||
|
s, e, r, g, b, a = color:find(
|
||||||
|
"rgba?%((%d+)%D+(%d+)%D+(%d+)[%s,]-([%.%d]-)%s-%)"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not s then return nil end
|
||||||
|
|
||||||
|
r = tonumber(r or "", base)
|
||||||
|
g = tonumber(g or "", base)
|
||||||
|
b = tonumber(b or "", base)
|
||||||
|
|
||||||
|
a = tonumber(a or "", base)
|
||||||
|
if a ~= nil then
|
||||||
|
if base ~= 16 then
|
||||||
|
a = a * 0xff
|
||||||
|
end
|
||||||
|
else
|
||||||
|
a = 0xff
|
||||||
|
end
|
||||||
|
|
||||||
|
if nibbles then
|
||||||
|
r = r * 16
|
||||||
|
g = g * 16
|
||||||
|
b = b * 16
|
||||||
|
end
|
||||||
|
|
||||||
|
return {r, g, b, a}
|
||||||
|
end
|
||||||
|
|
||||||
|
---Gets a color between two given colors on the position
|
||||||
|
---defined by the given percent.
|
||||||
|
---@param from_color renderer.color
|
||||||
|
---@param to_color renderer.color
|
||||||
|
---@param percent number
|
||||||
|
---@return renderer.color color
|
||||||
|
function ColorPicker.color_in_between(from_color, to_color, percent)
|
||||||
|
local color = {}
|
||||||
|
for i=1, 4 do
|
||||||
|
if from_color[i] == to_color[i] then
|
||||||
|
color[i] = from_color[i]
|
||||||
|
else
|
||||||
|
color[i] = common.clamp(
|
||||||
|
from_color[i] + math.floor((to_color[i] - from_color[i]) * percent),
|
||||||
|
0,
|
||||||
|
255
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return color
|
||||||
|
end
|
||||||
|
|
||||||
|
function ColorPicker:get_name()
|
||||||
|
return "Color Picker"
|
||||||
|
end
|
||||||
|
|
||||||
|
---Gets the currently selected color on the hue bar.
|
||||||
|
---@return renderer.color
|
||||||
|
function ColorPicker:get_hue_color()
|
||||||
|
local w = self.selector.w
|
||||||
|
local pos = self.hue_pos
|
||||||
|
local pos_percent = pos / 100
|
||||||
|
local range_size = w / 6
|
||||||
|
local range
|
||||||
|
if pos <= (HUE_COLOR_SEGMENT) then
|
||||||
|
range = 1
|
||||||
|
elseif pos <= (HUE_COLOR_SEGMENT) * 2 then
|
||||||
|
range = 2
|
||||||
|
elseif pos <= (HUE_COLOR_SEGMENT) * 3 then
|
||||||
|
range = 3
|
||||||
|
elseif pos <= (HUE_COLOR_SEGMENT) * 4 then
|
||||||
|
range = 4
|
||||||
|
elseif pos <= (HUE_COLOR_SEGMENT) * 5 then
|
||||||
|
range = 5
|
||||||
|
else
|
||||||
|
range = 6
|
||||||
|
end
|
||||||
|
local range_position = (w * pos_percent) - ((range_size * range)-range_size)
|
||||||
|
local range_percent = range_position / range_size
|
||||||
|
return ColorPicker.color_in_between(
|
||||||
|
HUE_COLOR_RANGES[range][1],
|
||||||
|
HUE_COLOR_RANGES[range][2],
|
||||||
|
range_percent
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Gets the currently selected color on the saturation bar.
|
||||||
|
---@return renderer.color
|
||||||
|
function ColorPicker:get_saturation_color()
|
||||||
|
local w = self.selector.w
|
||||||
|
local pos = self.saturation_pos
|
||||||
|
local pos_percent = pos / 100
|
||||||
|
local range_size = w
|
||||||
|
local range, color1, color2 = 1, COLOR_WHITE, self.hue_color
|
||||||
|
local range_position = (w * pos_percent) - ((range_size * range)-range_size)
|
||||||
|
local range_percent = range_position / range_size
|
||||||
|
return ColorPicker.color_in_between(color1, color2, range_percent)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Gets the currently selected color on the brightness bar.
|
||||||
|
---@return renderer.color
|
||||||
|
function ColorPicker:get_brightness_color()
|
||||||
|
local w = self.selector.w
|
||||||
|
local pos = self.brightness_pos
|
||||||
|
local pos_percent = pos / 100
|
||||||
|
local range_size = w
|
||||||
|
local range, color1, color2 = 1, COLOR_BLACK, self.saturation_color
|
||||||
|
local range_position = (w * pos_percent) - ((range_size * range)-range_size)
|
||||||
|
local range_percent = range_position / range_size
|
||||||
|
return ColorPicker.color_in_between(color1, color2, range_percent)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Gets the currently selected rgba color.
|
||||||
|
---@return renderer.color
|
||||||
|
function ColorPicker:get_color()
|
||||||
|
return ColorPicker.hsv_to_rgb(
|
||||||
|
self.hue_pos / 100,
|
||||||
|
self.saturation_pos / 100,
|
||||||
|
self.brightness_pos / 100,
|
||||||
|
self.alpha / 255
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set current color from rgba source which can also
|
||||||
|
---be a css string representation.
|
||||||
|
---@param color renderer.color | string
|
||||||
|
function ColorPicker:set_color(color, skip_html, skip_rgba)
|
||||||
|
-- we set the color on a coroutine in case it is been set before
|
||||||
|
-- the control is properly initialized like the constructor.
|
||||||
|
core.add_thread(function()
|
||||||
|
if type(color) == "string" then
|
||||||
|
color = ColorPicker.color_from_string(color)
|
||||||
|
end
|
||||||
|
|
||||||
|
if not color then color = {255, 0, 0, 255} end
|
||||||
|
|
||||||
|
local hsva = ColorPicker.rgb_to_hsv(color)
|
||||||
|
|
||||||
|
self.hue_pos = hsva[1] * 100
|
||||||
|
self.saturation_pos = hsva[2] * 100
|
||||||
|
self.brightness_pos = hsva[3] * 100
|
||||||
|
|
||||||
|
self.hue_color = self:get_hue_color()
|
||||||
|
self.saturation_color = self:get_saturation_color()
|
||||||
|
self.brightness_color = self:get_brightness_color()
|
||||||
|
self.alpha = color[4]
|
||||||
|
|
||||||
|
if not skip_html then
|
||||||
|
self.html_updating = true
|
||||||
|
self.html_notation:set_text(string.format(
|
||||||
|
"#%02X%02X%02X%02X",
|
||||||
|
color[1], color[2], color[3], color[4]
|
||||||
|
))
|
||||||
|
self.html_updating = false
|
||||||
|
end
|
||||||
|
|
||||||
|
if not skip_rgba then
|
||||||
|
self.rgba_updating = true
|
||||||
|
self.rgba_notation:set_text(string.format(
|
||||||
|
"rgba(%d,%d,%d,%.2f)",
|
||||||
|
color[1], color[2], color[3], color[4] / 255
|
||||||
|
))
|
||||||
|
self.rgba_updating = false
|
||||||
|
end
|
||||||
|
|
||||||
|
self:on_change(color)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set the transparency level, the lower the given alpha the more transparent.
|
||||||
|
---@param alpha number A value from 0 to 255
|
||||||
|
function ColorPicker:set_alpha(alpha)
|
||||||
|
self.alpha = common.clamp(alpha, 0, 255)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Draw a hue color bar at given location and size.
|
||||||
|
---@param x number
|
||||||
|
---@param y number
|
||||||
|
---@param w number
|
||||||
|
---@param h number
|
||||||
|
function ColorPicker:draw_hue(x, y, w, h)
|
||||||
|
local sx = x
|
||||||
|
local step = 1
|
||||||
|
local cwidth = 1
|
||||||
|
local cheight = h or 10
|
||||||
|
|
||||||
|
if w < 255*6 then
|
||||||
|
step = 255 / (w / 6)
|
||||||
|
else
|
||||||
|
cwidth = (w / 6) / 255
|
||||||
|
end
|
||||||
|
|
||||||
|
-- red -> yellow
|
||||||
|
for g=0, 255, step do
|
||||||
|
renderer.draw_rect(x, y, cwidth, cheight, {255, g, 0, 255})
|
||||||
|
x = x + cwidth
|
||||||
|
end
|
||||||
|
|
||||||
|
-- yellow -> green
|
||||||
|
for r=255, 0, -step do
|
||||||
|
renderer.draw_rect(x, y, cwidth, cheight, {r, 255, 0, 255})
|
||||||
|
x = x + cwidth
|
||||||
|
end
|
||||||
|
|
||||||
|
-- green -> cyan
|
||||||
|
for b=0, 255, step do
|
||||||
|
renderer.draw_rect(x, y, cwidth, cheight, {0, 255, b, 255})
|
||||||
|
x = x + cwidth
|
||||||
|
end
|
||||||
|
|
||||||
|
-- cyan -> blue
|
||||||
|
for g=255, 0, -step do
|
||||||
|
renderer.draw_rect(x, y, cwidth, cheight, {0, g, 255, 255})
|
||||||
|
x = x + cwidth
|
||||||
|
end
|
||||||
|
|
||||||
|
-- blue -> purple
|
||||||
|
for r=0, 255, step do
|
||||||
|
renderer.draw_rect(x, y, cwidth, cheight, {r, 0, 255, 255})
|
||||||
|
x = x + cwidth
|
||||||
|
end
|
||||||
|
|
||||||
|
-- purple -> red
|
||||||
|
for b=255, 0, -step do
|
||||||
|
renderer.draw_rect(x, y, cwidth, cheight, {255, 0, b, 255})
|
||||||
|
x = x + cwidth
|
||||||
|
end
|
||||||
|
|
||||||
|
sx = sx + (w * (self.hue_pos / 100))
|
||||||
|
self:draw_selector(sx, y, cheight, self.hue_color)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Draw a saturation color bar at given location and size.
|
||||||
|
---@param x number
|
||||||
|
---@param y number
|
||||||
|
---@param w number
|
||||||
|
---@param h number
|
||||||
|
function ColorPicker:draw_saturation(x, y, w, h)
|
||||||
|
local sx = x
|
||||||
|
local step = 1
|
||||||
|
local cwidth = 1
|
||||||
|
local cheight = h or 10
|
||||||
|
|
||||||
|
if w < 255 then
|
||||||
|
step = 255 / w
|
||||||
|
else
|
||||||
|
cwidth = w / 255
|
||||||
|
end
|
||||||
|
|
||||||
|
-- white to base
|
||||||
|
for i=0, 255, step do
|
||||||
|
local color = ColorPicker.color_in_between(COLOR_WHITE, self.hue_color, i / 255)
|
||||||
|
renderer.draw_rect(x, y, cwidth, cheight, color)
|
||||||
|
x = x + cwidth
|
||||||
|
end
|
||||||
|
|
||||||
|
sx = sx + (w * (self.saturation_pos / 100))
|
||||||
|
self:draw_selector(sx, y, cheight, self.saturation_color)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Draw a brightness color bar at given location and size.
|
||||||
|
---@param x number
|
||||||
|
---@param y number
|
||||||
|
---@param w number
|
||||||
|
---@param h number
|
||||||
|
function ColorPicker:draw_brightness(x, y, w, h)
|
||||||
|
local sx = x
|
||||||
|
local step = 1
|
||||||
|
local cwidth = 1
|
||||||
|
local cheight = h or 10
|
||||||
|
|
||||||
|
if w < 255 then
|
||||||
|
step = 255 / w
|
||||||
|
else
|
||||||
|
cwidth = w / 255
|
||||||
|
end
|
||||||
|
|
||||||
|
-- black to base
|
||||||
|
for i=0, 255, step do
|
||||||
|
local color = ColorPicker.color_in_between(COLOR_BLACK, self.saturation_color, i / 255)
|
||||||
|
renderer.draw_rect(x, y, cwidth, cheight, color)
|
||||||
|
x = x + cwidth
|
||||||
|
end
|
||||||
|
|
||||||
|
sx = sx + (w * (self.brightness_pos / 100))
|
||||||
|
self:draw_selector(sx, y, cheight, self.brightness_color)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param self widget.colorpicker
|
||||||
|
local function update_control_values(self)
|
||||||
|
local color = self:get_color()
|
||||||
|
self.alpha = color[4]
|
||||||
|
self.html_updating = true
|
||||||
|
self.rgba_updating = true
|
||||||
|
self.html_notation:set_text(string.format(
|
||||||
|
"#%02X%02X%02X%02X",
|
||||||
|
color[1], color[2], color[3], color[4]
|
||||||
|
))
|
||||||
|
self.rgba_notation:set_text(string.format(
|
||||||
|
"rgba(%d,%d,%d,%.2f)",
|
||||||
|
color[1], color[2], color[3], color[4] / 255
|
||||||
|
))
|
||||||
|
self.html_updating = false
|
||||||
|
self.rgba_updating = false
|
||||||
|
self:on_change(color)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ColorPicker:on_mouse_pressed(button, x, y, clicks)
|
||||||
|
if not ColorPicker.super.on_mouse_pressed(self, button, x, y, clicks) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if
|
||||||
|
x >= self.selector.x and x <= self.selector.x + self.selector.w
|
||||||
|
and
|
||||||
|
y >= self.selector.y and y <= self.selector.y + self.selector.h
|
||||||
|
then
|
||||||
|
local sx, sw = self.selector.x, self.selector.w
|
||||||
|
self.hue_pos = common.clamp(x, sx, sx + sw)
|
||||||
|
self.hue_pos = ((self.hue_pos - self.selector.x) / self.selector.w) * 100
|
||||||
|
self.hue_color = self:get_hue_color()
|
||||||
|
self.saturation_color = self:get_saturation_color()
|
||||||
|
self.hue_mouse_down = true
|
||||||
|
elseif
|
||||||
|
x >= self.selector.x and x <= self.selector.x + self.selector.w
|
||||||
|
and
|
||||||
|
y >= self.selector.y + style.padding.y + self.selector.h
|
||||||
|
and
|
||||||
|
y <= self.selector.y + style.padding.y + (self.selector.h * 2)
|
||||||
|
then
|
||||||
|
local sx, sw = self.selector.x, self.selector.w
|
||||||
|
self.saturation_pos = common.clamp(x, sx, sx + sw)
|
||||||
|
self.saturation_pos = ((self.saturation_pos - self.selector.x) / self.selector.w) * 100
|
||||||
|
self.saturation_color = self:get_saturation_color()
|
||||||
|
self.brightness_color = self:get_brightness_color()
|
||||||
|
self.saturation_mouse_down = true
|
||||||
|
elseif
|
||||||
|
x >= self.selector.x and x <= self.selector.x + self.selector.w
|
||||||
|
and
|
||||||
|
y >= self.selector.y + style.padding.y * 2 + self.selector.h * 2
|
||||||
|
and
|
||||||
|
y <= self.selector.y + style.padding.y * 2 + (self.selector.h * 4)
|
||||||
|
then
|
||||||
|
local sx, sw = self.selector.x, self.selector.w
|
||||||
|
self.brightness_pos = common.clamp(x, sx, sx + sw)
|
||||||
|
self.brightness_pos = ((self.brightness_pos - self.selector.x) / self.selector.w) * 100
|
||||||
|
self.brightness_color = self:get_brightness_color()
|
||||||
|
self.brightness_mouse_down = true
|
||||||
|
end
|
||||||
|
if self.hue_mouse_down or self.saturation_mouse_down or self.brightness_mouse_down then
|
||||||
|
self:capture_mouse()
|
||||||
|
update_control_values(self)
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function ColorPicker:on_mouse_released(button, x, y)
|
||||||
|
if self.hue_mouse_down or self.saturation_mouse_down or self.brightness_mouse_down then
|
||||||
|
self:release_mouse()
|
||||||
|
end
|
||||||
|
|
||||||
|
self.hue_mouse_down = false
|
||||||
|
self.saturation_mouse_down = false
|
||||||
|
self.brightness_mouse_down = false
|
||||||
|
|
||||||
|
if not ColorPicker.super.on_mouse_released(self, button, x, y) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function ColorPicker:on_mouse_moved(x, y, dx, dy)
|
||||||
|
if self.hue_mouse_down then
|
||||||
|
local sx, sw = self.selector.x, self.selector.w
|
||||||
|
self.hue_pos = common.clamp(x, sx, sx + sw)
|
||||||
|
self.hue_pos = ((self.hue_pos - self.selector.x) / self.selector.w) * 100
|
||||||
|
self.hue_color = self:get_hue_color()
|
||||||
|
self.saturation_color = self:get_saturation_color()
|
||||||
|
self.brightness_color = self:get_brightness_color()
|
||||||
|
elseif self.saturation_mouse_down then
|
||||||
|
local sx, sw = self.selector.x, self.selector.w
|
||||||
|
self.saturation_pos = common.clamp(x, sx, sx + sw)
|
||||||
|
self.saturation_pos = ((self.saturation_pos - self.selector.x) / self.selector.w) * 100
|
||||||
|
self.saturation_color = self:get_saturation_color()
|
||||||
|
self.brightness_color = self:get_brightness_color()
|
||||||
|
elseif self.brightness_mouse_down then
|
||||||
|
local sx, sw = self.selector.x, self.selector.w
|
||||||
|
self.brightness_pos = common.clamp(x, sx, sx + sw)
|
||||||
|
self.brightness_pos = ((self.brightness_pos - self.selector.x) / self.selector.w) * 100
|
||||||
|
self.brightness_color = self:get_brightness_color()
|
||||||
|
end
|
||||||
|
if self.hue_mouse_down or self.saturation_mouse_down or self.brightness_mouse_down then
|
||||||
|
update_control_values(self)
|
||||||
|
else
|
||||||
|
return ColorPicker.super.on_mouse_moved(self, x, y, dx, dy)
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function ColorPicker:update_size()
|
||||||
|
self.selector.h = 10 * SCALE
|
||||||
|
local x, y = 0, style.padding.y * 3 + self.selector.h * 4
|
||||||
|
self.html_notation:set_position(x, y)
|
||||||
|
self.rgba_notation:set_position(self.html_notation:get_right() + style.padding.x, y)
|
||||||
|
if self:get_width() < self.rgba_notation:get_right() then
|
||||||
|
self:set_size(self.rgba_notation:get_right() + style.padding.x)
|
||||||
|
end
|
||||||
|
if self:get_height() < self.rgba_notation:get_bottom() then
|
||||||
|
self:set_size(nil, self.rgba_notation:get_bottom() + style.padding.y)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ColorPicker:update()
|
||||||
|
if not ColorPicker.super.update(self) then return false end
|
||||||
|
self:update_size()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function ColorPicker:draw_selector(x, y, h, color)
|
||||||
|
local border = 2 * SCALE
|
||||||
|
x = x - border - ((10 * SCALE) / border)
|
||||||
|
y = y - border
|
||||||
|
renderer.draw_rect(x, y, 14 * SCALE, h + (border * 2), COLOR_WHITE)
|
||||||
|
renderer.draw_rect(x+border, y + border, 10 * SCALE, h, color)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ColorPicker:draw()
|
||||||
|
if not ColorPicker.super.draw(self) then return false end
|
||||||
|
|
||||||
|
local x, y, w = self.position.x, self.position.y, self.size.x
|
||||||
|
|
||||||
|
self.selector.x = x
|
||||||
|
self.selector.y = style.padding.y + y
|
||||||
|
self.selector.w = w - style.padding.x - 100
|
||||||
|
self.selector.h = 10 * SCALE
|
||||||
|
|
||||||
|
self:draw_hue(
|
||||||
|
self.selector.x,
|
||||||
|
self.selector.y,
|
||||||
|
self.selector.w,
|
||||||
|
self.selector.h
|
||||||
|
)
|
||||||
|
|
||||||
|
self:draw_saturation(
|
||||||
|
self.selector.x,
|
||||||
|
self.selector.y + style.padding.y + self.selector.h,
|
||||||
|
self.selector.w,
|
||||||
|
self.selector.h
|
||||||
|
)
|
||||||
|
|
||||||
|
self:draw_brightness(
|
||||||
|
self.selector.x,
|
||||||
|
self.selector.y + style.padding.y * 2 + self.selector.h * 2,
|
||||||
|
self.selector.w,
|
||||||
|
self.selector.h
|
||||||
|
)
|
||||||
|
|
||||||
|
local c = self:get_color()
|
||||||
|
|
||||||
|
renderer.draw_rect(
|
||||||
|
self.selector.x + style.padding.x + self.selector.w,
|
||||||
|
self.selector.y,
|
||||||
|
100,
|
||||||
|
(self.selector.y + style.padding.y * 2 + self.selector.h * 3)
|
||||||
|
- self.selector.y,
|
||||||
|
c
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return ColorPicker
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
--
|
||||||
|
-- Color Picker Dialog Widget.
|
||||||
|
-- @copyright Jefferson Gonzalez
|
||||||
|
-- @license MIT
|
||||||
|
--
|
||||||
|
|
||||||
|
local style = require "core.style"
|
||||||
|
local Button = require "libraries.widget.button"
|
||||||
|
local ColorPicker = require "libraries.widget.colorpicker"
|
||||||
|
local Dialog = require "libraries.widget.dialog"
|
||||||
|
|
||||||
|
---@class widget.colorpickerdialog : widget.dialog
|
||||||
|
---@overload fun(title?:string, color?:renderer.color|string):widget.colorpickerdialog
|
||||||
|
---@field super widget.dialog
|
||||||
|
---@field picker widget.colorpicker
|
||||||
|
---@field apply widget.button
|
||||||
|
---@field cancel widget.button
|
||||||
|
local ColorPickerDialog = Dialog:extend()
|
||||||
|
|
||||||
|
---Constructor
|
||||||
|
---@param title? string
|
||||||
|
---@param color? renderer.color | string
|
||||||
|
function ColorPickerDialog:new(title, color)
|
||||||
|
ColorPickerDialog.super.new(self, title or "Color Picker")
|
||||||
|
|
||||||
|
self.type_name = "widget.colorpickerdialog"
|
||||||
|
self.picker = ColorPicker(self.panel, color)
|
||||||
|
|
||||||
|
local this = self
|
||||||
|
|
||||||
|
self.apply = Button(self.panel, "Apply")
|
||||||
|
self.apply:set_icon("S")
|
||||||
|
function self.apply:on_click()
|
||||||
|
this:on_apply(this.picker:get_color())
|
||||||
|
this:on_close()
|
||||||
|
end
|
||||||
|
|
||||||
|
self.cancel = Button(self.panel, "Cancel")
|
||||||
|
self.cancel:set_icon("C")
|
||||||
|
function self.cancel:on_click()
|
||||||
|
this:on_close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Called when the user clicks on apply
|
||||||
|
---@param value renderer.color
|
||||||
|
function ColorPickerDialog:on_apply(value) end
|
||||||
|
|
||||||
|
function ColorPickerDialog:update()
|
||||||
|
if not ColorPickerDialog.super.update(self) then return false end
|
||||||
|
|
||||||
|
self.picker:set_position(style.padding.x/2, 0)
|
||||||
|
|
||||||
|
self.apply:set_position(
|
||||||
|
style.padding.x/2,
|
||||||
|
self.picker:get_bottom() + style.padding.y
|
||||||
|
)
|
||||||
|
self.cancel:set_position(
|
||||||
|
self.apply:get_right() + style.padding.x,
|
||||||
|
self.picker:get_bottom() + style.padding.y
|
||||||
|
)
|
||||||
|
|
||||||
|
self.panel.size.x = self.panel:get_real_width() + style.padding.x
|
||||||
|
self.panel.size.y = self.panel:get_real_height()
|
||||||
|
self.size.x = self:get_real_width() - (style.padding.x / 2)
|
||||||
|
self.size.y = self:get_real_height() + (style.padding.y / 2)
|
||||||
|
|
||||||
|
self.close:set_position(
|
||||||
|
self.size.x - self.close.size.x - (style.padding.x / 2),
|
||||||
|
style.padding.y / 2
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return ColorPickerDialog
|
||||||
+164
@@ -0,0 +1,164 @@
|
|||||||
|
--
|
||||||
|
-- Dialog object that serves as base to implement other dialogs.
|
||||||
|
-- @copyright Jefferson Gonzalez
|
||||||
|
-- @license MIT
|
||||||
|
--
|
||||||
|
|
||||||
|
local core = require "core"
|
||||||
|
local style = require "core.style"
|
||||||
|
local Widget = require "libraries.widget"
|
||||||
|
local Button = require "libraries.widget.button"
|
||||||
|
local Label = require "libraries.widget.label"
|
||||||
|
|
||||||
|
---@class widget.dialog : widget
|
||||||
|
---@overload fun(title?:string):widget.dialog
|
||||||
|
---@field protected title widget.label
|
||||||
|
---@field protected close widget.button
|
||||||
|
---@field protected panel widget
|
||||||
|
local Dialog = Widget:extend()
|
||||||
|
|
||||||
|
---Constructor
|
||||||
|
---@param title string
|
||||||
|
function Dialog:new(title)
|
||||||
|
Dialog.super.new(self)
|
||||||
|
|
||||||
|
self.type_name = "widget.dialog"
|
||||||
|
|
||||||
|
self.draggable = true
|
||||||
|
self.scrollable = false
|
||||||
|
|
||||||
|
-- minimum width and height
|
||||||
|
self.size.mx = 400
|
||||||
|
self.size.my = 150
|
||||||
|
|
||||||
|
self.title = Label(self, "")
|
||||||
|
self.close = Button(self, "")
|
||||||
|
self.close:set_icon("X")
|
||||||
|
self.close.border.width = 0
|
||||||
|
self.close:toggle_background(false)
|
||||||
|
self.close.padding.x = 4
|
||||||
|
self.close.padding.y = 0
|
||||||
|
self.panel = Widget(self)
|
||||||
|
self.panel.border.width = 0
|
||||||
|
self.panel.scrollable = true
|
||||||
|
|
||||||
|
local this = self
|
||||||
|
|
||||||
|
function self.close:on_click()
|
||||||
|
this:on_close()
|
||||||
|
this:hide()
|
||||||
|
end
|
||||||
|
|
||||||
|
self:set_title(title or "")
|
||||||
|
end
|
||||||
|
|
||||||
|
---Returns the widget where you can add child widgets to this dialog.
|
||||||
|
---@return widget
|
||||||
|
function Dialog:get_panel()
|
||||||
|
return self.panel
|
||||||
|
end
|
||||||
|
|
||||||
|
---Change the dialog title.
|
||||||
|
---@param text string|widget.styledtext
|
||||||
|
function Dialog:set_title(text)
|
||||||
|
self.title:set_label(text)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Calculate the dialog size, centers it relative to screen and shows it.
|
||||||
|
function Dialog:show()
|
||||||
|
Dialog.super.show(self)
|
||||||
|
self:update()
|
||||||
|
self:centered()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Called when the user clicks the close button of the dialog.
|
||||||
|
function Dialog:on_close()
|
||||||
|
self:hide()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Dialog:update()
|
||||||
|
if not Dialog.super.update(self) then return false end
|
||||||
|
|
||||||
|
local width = math.max(
|
||||||
|
self.title:get_width() + (style.padding.x * 3) + self.close:get_width(),
|
||||||
|
self.size.mx,
|
||||||
|
self.size.x
|
||||||
|
)
|
||||||
|
|
||||||
|
local height = math.max(
|
||||||
|
self.title:get_height() + (style.padding.y * 3),
|
||||||
|
self.size.my,
|
||||||
|
self.size.y
|
||||||
|
)
|
||||||
|
|
||||||
|
self:set_size(width, height)
|
||||||
|
|
||||||
|
self.title:set_position(
|
||||||
|
style.padding.x / 2,
|
||||||
|
style.padding.y / 2
|
||||||
|
)
|
||||||
|
|
||||||
|
self.close:set_position(
|
||||||
|
self.size.x - self.close.size.x - (style.padding.x / 2),
|
||||||
|
style.padding.y / 2
|
||||||
|
)
|
||||||
|
|
||||||
|
self.panel:set_position(
|
||||||
|
0,
|
||||||
|
self.title:get_bottom() + (style.padding.y / 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.panel:set_size(
|
||||||
|
self.size.x,
|
||||||
|
self.size.y - self.title.size.y - style.padding.y
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
---We overwrite default draw function to draw the title background.
|
||||||
|
function Dialog:draw()
|
||||||
|
if not self:is_visible() then return false end
|
||||||
|
|
||||||
|
Dialog.super.draw(self)
|
||||||
|
|
||||||
|
self:draw_border()
|
||||||
|
|
||||||
|
if self.background_color then
|
||||||
|
self:draw_background(self.background_color)
|
||||||
|
else
|
||||||
|
self:draw_background(
|
||||||
|
self.parent and style.background or style.background2
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
if #self.childs > 0 then
|
||||||
|
core.push_clip_rect(
|
||||||
|
self.position.x,
|
||||||
|
self.position.y,
|
||||||
|
self.size.x,
|
||||||
|
self.size.y
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- draw the title background
|
||||||
|
renderer.draw_rect(
|
||||||
|
self.position.x,
|
||||||
|
self.position.y,
|
||||||
|
self.size.x, self.title:get_height() + style.padding.y,
|
||||||
|
style.selection
|
||||||
|
)
|
||||||
|
|
||||||
|
for i=#self.childs, 1, -1 do
|
||||||
|
self.childs[i]:draw()
|
||||||
|
end
|
||||||
|
|
||||||
|
if #self.childs > 0 then
|
||||||
|
core.pop_clip_rect()
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return Dialog
|
||||||
+395
@@ -0,0 +1,395 @@
|
|||||||
|
local core = require "core"
|
||||||
|
local common = require "core.common"
|
||||||
|
local style = require "core.style"
|
||||||
|
local Widget = require "libraries.widget"
|
||||||
|
local Button = require "libraries.widget.button"
|
||||||
|
local Label = require "libraries.widget.label"
|
||||||
|
|
||||||
|
---@class widget.filepicker : widget
|
||||||
|
---@overload fun(parent?:widget, path?:string):widget.filepicker
|
||||||
|
---@field public pick_mode integer
|
||||||
|
---@field public filters table<integer,string>
|
||||||
|
---@field private path string
|
||||||
|
---@field private file widget.label
|
||||||
|
---@field private textbox widget.textbox
|
||||||
|
---@field private button widget.button
|
||||||
|
local FilePicker = Widget:extend()
|
||||||
|
|
||||||
|
---Operation modes for the file picker.
|
||||||
|
---@type table<string,integer>
|
||||||
|
FilePicker.mode = {
|
||||||
|
---Opens file browser the selected file does not has to exist.
|
||||||
|
FILE = 1,
|
||||||
|
---Opens file browser the selected file has to exist.
|
||||||
|
FILE_EXISTS = 2,
|
||||||
|
---Opens directory browser the selected directory does not has to exist.
|
||||||
|
DIRECTORY = 4,
|
||||||
|
---Opens directory browser the selected directory has to exist.
|
||||||
|
DIRECTORY_EXISTS = 8
|
||||||
|
}
|
||||||
|
|
||||||
|
---@param text string
|
||||||
|
local function suggest_directory(text)
|
||||||
|
text = common.home_expand(text)
|
||||||
|
return common.home_encode_list(common.dir_path_suggest(text))
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param path string
|
||||||
|
local function check_directory_path(path)
|
||||||
|
local abs_path = system.absolute_path(path)
|
||||||
|
local info = abs_path and system.get_file_info(abs_path)
|
||||||
|
if not info or info.type ~= 'dir' then return nil end
|
||||||
|
return abs_path
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param str string
|
||||||
|
---@param find string
|
||||||
|
---@param replace string
|
||||||
|
local function str_replace(str, find, replace)
|
||||||
|
local start, ending = str:find(find, 1, true)
|
||||||
|
if start == 1 then
|
||||||
|
return replace .. str:sub(ending + 1)
|
||||||
|
else
|
||||||
|
return str:sub(1, start - 1) .. replace .. str:sub(ending + 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@alias widget.filepicker.modes
|
||||||
|
---| `FilePicker.mode.FILE`
|
||||||
|
---| `FilePicker.mode.FILE_EXISTS`
|
||||||
|
---| `FilePicker.mode.DIRECTORY`
|
||||||
|
---| `FilePicker.mode.DIRECTORY_EXISTS`
|
||||||
|
|
||||||
|
---Constructor
|
||||||
|
---@param parent widget
|
||||||
|
---@param path? string
|
||||||
|
function FilePicker:new(parent, path)
|
||||||
|
FilePicker.super.new(self, parent)
|
||||||
|
|
||||||
|
local this = self
|
||||||
|
|
||||||
|
self.type_name = "widget.filepicker"
|
||||||
|
|
||||||
|
self.filters = {}
|
||||||
|
self.border.width = 0
|
||||||
|
self.pick_mode = FilePicker.mode.FILE
|
||||||
|
|
||||||
|
self.file = Label(self, "")
|
||||||
|
self.file.clickable = true
|
||||||
|
self.file:set_border_width(1)
|
||||||
|
function self.file:on_click(button)
|
||||||
|
if button == "left" then
|
||||||
|
this:show_picker()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function self.file:on_mouse_enter(...)
|
||||||
|
Label.super.on_mouse_enter(self, ...)
|
||||||
|
self.border.color = style.caret
|
||||||
|
end
|
||||||
|
function self.file:on_mouse_leave(...)
|
||||||
|
Label.super.on_mouse_leave(self, ...)
|
||||||
|
self.border.color = style.text
|
||||||
|
end
|
||||||
|
|
||||||
|
self.button = Button(self, "")
|
||||||
|
self.button:set_icon("D")
|
||||||
|
self.button:set_tooltip("open file browser")
|
||||||
|
function self.button:on_click(button)
|
||||||
|
if button == "left" then
|
||||||
|
this:show_picker()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local label_width = self.file:get_width()
|
||||||
|
if label_width <= 10 then
|
||||||
|
label_width = 200 + (self.file.border.width * 2)
|
||||||
|
self.file:set_size(200, self.button:get_height() - self.button.border.width * 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
self:set_size(
|
||||||
|
label_width + self.button:get_width(),
|
||||||
|
math.max(self.file:get_height(), self.button:get_height())
|
||||||
|
)
|
||||||
|
|
||||||
|
self:set_path(path)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set the filepicker size
|
||||||
|
---@param width? number
|
||||||
|
---@param height? number
|
||||||
|
function FilePicker:set_size(width, height)
|
||||||
|
FilePicker.super.set_size(self, width, height)
|
||||||
|
|
||||||
|
self.file:set_position(0, 0)
|
||||||
|
self.file:set_size(
|
||||||
|
self:get_width() - self.button:get_width(),
|
||||||
|
self.button:get_height()
|
||||||
|
)
|
||||||
|
|
||||||
|
self.button:set_position(self.file:get_right(), 0)
|
||||||
|
|
||||||
|
self.size.y = math.max(
|
||||||
|
self.file:get_height(),
|
||||||
|
self.button:get_height()
|
||||||
|
-- something is off on calculation since adding border width should not
|
||||||
|
-- be needed to display whole rendered control at all...
|
||||||
|
) + self.button.border.width
|
||||||
|
end
|
||||||
|
|
||||||
|
---Add a lua pattern to the filters list
|
||||||
|
---@param pattern string
|
||||||
|
function FilePicker:add_filter(pattern)
|
||||||
|
table.insert(self.filters, pattern)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Clear the filters list
|
||||||
|
function FilePicker:clear_filters()
|
||||||
|
self.filters = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set the operation mode for the file picker.
|
||||||
|
---@param mode widget.filepicker.modes | string | integer
|
||||||
|
function FilePicker:set_mode(mode)
|
||||||
|
if type(mode) == "string" then
|
||||||
|
---@type integer
|
||||||
|
local intmode = FilePicker.mode[mode:upper()]
|
||||||
|
self.pick_mode = intmode
|
||||||
|
else
|
||||||
|
self.pick_mode = mode
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set the full path including directory and filename.
|
||||||
|
---@param path? string
|
||||||
|
function FilePicker:set_path(path)
|
||||||
|
if path then
|
||||||
|
self.path = path or ""
|
||||||
|
if common.path_belongs_to(path, core.project_dir) then
|
||||||
|
self.file.label = path ~= "" and
|
||||||
|
common.relative_path(core.project_dir, path)
|
||||||
|
or
|
||||||
|
""
|
||||||
|
else
|
||||||
|
self.file.label = path
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self.path = ""
|
||||||
|
self.file.label = ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the full path including directory and filename.
|
||||||
|
---@return string | nil
|
||||||
|
function FilePicker:get_path()
|
||||||
|
if self.path ~= "" then
|
||||||
|
return self.path
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the full path relative to current project dir or absolute if it doesn't
|
||||||
|
---belongs to the current project directory.
|
||||||
|
---@return string
|
||||||
|
function FilePicker:get_relative_path()
|
||||||
|
if
|
||||||
|
self.path ~= ""
|
||||||
|
and
|
||||||
|
common.path_belongs_to(self.path, core.project_dir)
|
||||||
|
then
|
||||||
|
return common.relative_path(core.project_dir, self.path)
|
||||||
|
end
|
||||||
|
return self.path or ""
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set the filename part only.
|
||||||
|
---@param name string
|
||||||
|
function FilePicker:set_filename(name)
|
||||||
|
local dir_part = common.dirname(self.path)
|
||||||
|
if dir_part then
|
||||||
|
self:set_path(dir_part .. PATHSEP .. name)
|
||||||
|
else
|
||||||
|
self:set_path(name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the filename part only.
|
||||||
|
---@return string | nil
|
||||||
|
function FilePicker:get_filename()
|
||||||
|
local dir_part = common.dirname(self.path)
|
||||||
|
if dir_part then
|
||||||
|
local filename = str_replace(self.path, dir_part .. PATHSEP, "")
|
||||||
|
return filename
|
||||||
|
elseif self.path ~= "" then
|
||||||
|
return self.path
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set the directory part only.
|
||||||
|
---@param dir string
|
||||||
|
function FilePicker:set_directory(dir)
|
||||||
|
local filename = self:get_filename()
|
||||||
|
if filename then
|
||||||
|
self:set_path(dir:gsub("[\\/]$", "") .. PATHSEP .. filename)
|
||||||
|
else
|
||||||
|
self:set_path(dir:gsub("[\\/]$", ""))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the directory part only.
|
||||||
|
---@return string | nil
|
||||||
|
function FilePicker:get_directory()
|
||||||
|
if self.path ~= "" then
|
||||||
|
local dir_part = common.dirname(self.path)
|
||||||
|
if dir_part then return dir_part end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---Filter a list of directories by applying currently set filters.
|
||||||
|
---@param self widget.filepicker
|
||||||
|
---@param list table<integer, string>
|
||||||
|
---@return table<integer,string>
|
||||||
|
local function filter(self, list)
|
||||||
|
if #self.filters > 0 then
|
||||||
|
local new_list = {}
|
||||||
|
for _, value in ipairs(list) do
|
||||||
|
if common.match_pattern(value, self.filters) then
|
||||||
|
table.insert(new_list, value)
|
||||||
|
elseif
|
||||||
|
self.pick_mode == FilePicker.mode.FILE
|
||||||
|
or
|
||||||
|
self.pick_mode == FilePicker.mode.FILE_EXISTS
|
||||||
|
then
|
||||||
|
local path = common.home_expand(value)
|
||||||
|
local abs_path = check_directory_path(path)
|
||||||
|
if abs_path then
|
||||||
|
table.insert(new_list, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return new_list
|
||||||
|
end
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param self widget.filepicker
|
||||||
|
local function show_file_picker(self)
|
||||||
|
core.command_view:enter("Choose File", {
|
||||||
|
text = self:get_relative_path(),
|
||||||
|
submit = function(text)
|
||||||
|
---@type string
|
||||||
|
local filename = text
|
||||||
|
local dirname = common.dirname(common.home_expand(text))
|
||||||
|
if dirname then
|
||||||
|
filename = common.home_expand(text)
|
||||||
|
filename = system.absolute_path(dirname)
|
||||||
|
.. PATHSEP
|
||||||
|
.. str_replace(filename, dirname .. PATHSEP, "")
|
||||||
|
elseif filename ~= "" then
|
||||||
|
filename = core.project_dir .. PATHSEP .. filename
|
||||||
|
end
|
||||||
|
self:set_path(filename)
|
||||||
|
self:on_change(filename ~= "" and filename or nil)
|
||||||
|
end,
|
||||||
|
suggest = function (text)
|
||||||
|
return filter(
|
||||||
|
self,
|
||||||
|
common.home_encode_list(common.path_suggest(common.home_expand(text)))
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
validate = function(text)
|
||||||
|
if #self.filters > 0 and text ~= "" and not common.match_pattern(text, self.filters) then
|
||||||
|
core.error(
|
||||||
|
"File does not match the filters: %s",
|
||||||
|
table.concat(self.filters, ", ")
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local filename = common.home_expand(text)
|
||||||
|
local path_stat, err = system.get_file_info(filename)
|
||||||
|
if path_stat and path_stat.type == 'dir' then
|
||||||
|
core.error("Cannot open %s, is a folder", text)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if self.pick_mode == FilePicker.mode.FILE_EXISTS then
|
||||||
|
if not path_stat then
|
||||||
|
core.error("Cannot open file %s: %s", text, err)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local dirname = common.dirname(filename)
|
||||||
|
local dir_stat = dirname and system.get_file_info(dirname)
|
||||||
|
if dirname and not dir_stat then
|
||||||
|
core.error("Directory does not exists: %s", dirname)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param self widget.filepicker
|
||||||
|
local function show_dir_picker(self)
|
||||||
|
core.command_view:enter("Choose Directory", {
|
||||||
|
text = self:get_relative_path(),
|
||||||
|
submit = function(text)
|
||||||
|
local path = common.home_expand(text)
|
||||||
|
local abs_path = check_directory_path(path)
|
||||||
|
self:set_path(abs_path or text)
|
||||||
|
self:on_change(abs_path or (text ~= "" and text or nil))
|
||||||
|
end,
|
||||||
|
suggest = function(text)
|
||||||
|
return filter(self, suggest_directory(text))
|
||||||
|
end,
|
||||||
|
validate = function(text)
|
||||||
|
if #self.filters > 0 and text ~= "" and not common.match_pattern(text, self.filters) then
|
||||||
|
core.error(
|
||||||
|
"Directory does not match the filters: %s",
|
||||||
|
table.concat(self.filters, ", ")
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if self.pick_mode == FilePicker.mode.DIRECTORY_EXISTS then
|
||||||
|
local path = common.home_expand(text)
|
||||||
|
local abs_path = check_directory_path(path)
|
||||||
|
if not abs_path then
|
||||||
|
core.error("Cannot open directory %q", path)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
---Show the command view file or directory browser depending on the
|
||||||
|
---current file picker mode.
|
||||||
|
function FilePicker:show_picker()
|
||||||
|
if
|
||||||
|
self.pick_mode == FilePicker.mode.FILE
|
||||||
|
or
|
||||||
|
self.pick_mode == FilePicker.mode.FILE_EXISTS
|
||||||
|
then
|
||||||
|
show_file_picker(self)
|
||||||
|
else
|
||||||
|
show_dir_picker(self)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function FilePicker:update()
|
||||||
|
if not FilePicker.super.update(self) then return false end
|
||||||
|
|
||||||
|
if self:get_width() ~= (self.file:get_width() + self.button:get_width()) then
|
||||||
|
self:set_size(
|
||||||
|
self.file:get_width() + self.button:get_width(),
|
||||||
|
self.button:get_height()
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return FilePicker
|
||||||
+216
@@ -0,0 +1,216 @@
|
|||||||
|
--
|
||||||
|
-- FoldingBook Widget.
|
||||||
|
-- @copyright Jefferson Gonzalez
|
||||||
|
-- @license MIT
|
||||||
|
--
|
||||||
|
|
||||||
|
local style = require "core.style"
|
||||||
|
local Widget = require "libraries.widget"
|
||||||
|
local Button = require "libraries.widget.button"
|
||||||
|
|
||||||
|
---Represents a foldingbook pane
|
||||||
|
---@class widget.foldingbook.pane
|
||||||
|
---@field public name string
|
||||||
|
---@field public tab widget.button
|
||||||
|
---@field public container widget
|
||||||
|
---@field public expanded boolean
|
||||||
|
local FoldingBookPane = {}
|
||||||
|
|
||||||
|
---@class widget.foldingbook : widget
|
||||||
|
---@overload fun(parent:widget?):widget.foldingbook
|
||||||
|
---@field public panes widget.foldingbook.pane[]
|
||||||
|
local FoldingBook = Widget:extend()
|
||||||
|
|
||||||
|
---FoldingBook constructor
|
||||||
|
---@param parent widget
|
||||||
|
function FoldingBook:new(parent)
|
||||||
|
FoldingBook.super.new(self, parent)
|
||||||
|
self.type_name = "widget.foldingbook"
|
||||||
|
self.panes = {}
|
||||||
|
self.scrollable = true
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param pane widget.foldingbook.pane
|
||||||
|
function FoldingBook:on_tab_click(pane)
|
||||||
|
pane.expanded = not pane.expanded
|
||||||
|
end
|
||||||
|
|
||||||
|
---Adds a new pane to the foldingbook and returns a container widget where
|
||||||
|
---you can add more child elements.
|
||||||
|
---@param name string
|
||||||
|
---@param label string
|
||||||
|
---@return widget container
|
||||||
|
function FoldingBook:add_pane(name, label)
|
||||||
|
---@type widget.button
|
||||||
|
local tab = Button(self, label)
|
||||||
|
tab.border.width = 0
|
||||||
|
tab:toggle_expand(true)
|
||||||
|
tab:set_icon("+")
|
||||||
|
|
||||||
|
if #self.panes > 0 then
|
||||||
|
if self.panes[#self.panes].expanded then
|
||||||
|
tab:set_position(0, self.panes[#self.panes].container:get_bottom() + 2)
|
||||||
|
else
|
||||||
|
tab:set_position(0, self.panes[#self.panes].tab:get_bottom() + 2)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
tab:set_position(0, 10)
|
||||||
|
end
|
||||||
|
|
||||||
|
local container = Widget(self)
|
||||||
|
container:set_position(0, tab:get_bottom() + 4)
|
||||||
|
container:set_size(self:get_width(), 0)
|
||||||
|
|
||||||
|
local pane = {
|
||||||
|
name = name,
|
||||||
|
tab = tab,
|
||||||
|
container = container,
|
||||||
|
expanded = false
|
||||||
|
}
|
||||||
|
|
||||||
|
tab.on_click = function()
|
||||||
|
self:on_tab_click(pane)
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(self.panes, pane)
|
||||||
|
|
||||||
|
return container
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param name string
|
||||||
|
---@return widget.foldingbook.pane | nil
|
||||||
|
function FoldingBook:get_pane(name)
|
||||||
|
for _, pane in pairs(self.panes) do
|
||||||
|
if pane.name == name then
|
||||||
|
return pane
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---Delete a pane and all its childs from the folding book.
|
||||||
|
---@param name string
|
||||||
|
---@return boolean deleted
|
||||||
|
function FoldingBook:delete_pane(name)
|
||||||
|
for idx, pane in ipairs(self.panes) do
|
||||||
|
if pane.name == name then
|
||||||
|
self:remove_child(pane.tab)
|
||||||
|
self:remove_child(pane.container)
|
||||||
|
table.remove(self.panes, idx)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
---Activates the given pane
|
||||||
|
---@param name string
|
||||||
|
---@param visible boolean | nil
|
||||||
|
function FoldingBook:toggle_pane(name, visible)
|
||||||
|
local pane = self:get_pane(name)
|
||||||
|
if pane then
|
||||||
|
if type(visible) == "boolean" then
|
||||||
|
pane.expanded = visible
|
||||||
|
else
|
||||||
|
pane.expanded = not pane.expanded
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Change the tab label of the given pane.
|
||||||
|
---@param name string
|
||||||
|
---@param label string
|
||||||
|
function FoldingBook:set_pane_label(name, label)
|
||||||
|
local pane = self:get_pane(name)
|
||||||
|
if pane then
|
||||||
|
pane.tab:set_label(label)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set or remove the icon for the given pane.
|
||||||
|
---@param name string
|
||||||
|
---@param icon string
|
||||||
|
---@param color? renderer.color|nil
|
||||||
|
---@param hover_color? renderer.color|nil
|
||||||
|
function FoldingBook:set_pane_icon(name, icon, color, hover_color)
|
||||||
|
local pane = self:get_pane(name)
|
||||||
|
if pane then
|
||||||
|
pane.tab:set_icon(icon, color, hover_color)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
---Recalculate the position of the elements on resizing or position changes.
|
||||||
|
function FoldingBook:update()
|
||||||
|
if not FoldingBook.super.update(self) then return false end
|
||||||
|
|
||||||
|
---@type widget.foldingbook.pane
|
||||||
|
local prev_pane = nil
|
||||||
|
|
||||||
|
for _, pane in ipairs(self.panes) do
|
||||||
|
local tx, ty = 0, 10
|
||||||
|
local cx, cy = 0, 0
|
||||||
|
local cw, ch = 0, 0
|
||||||
|
|
||||||
|
if prev_pane then
|
||||||
|
if prev_pane and prev_pane.container:is_visible() then
|
||||||
|
ty = prev_pane.container:get_bottom() + 2
|
||||||
|
else
|
||||||
|
ty = prev_pane.tab:get_bottom() + 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
pane.tab:set_position(tx, ty)
|
||||||
|
|
||||||
|
cy = pane.tab:get_bottom() + 4
|
||||||
|
cw = self:get_width()
|
||||||
|
if #pane.container.childs > 0 then
|
||||||
|
ch = pane.container:get_real_height() + 10
|
||||||
|
end
|
||||||
|
|
||||||
|
pane.container.border.color = style.divider
|
||||||
|
|
||||||
|
if pane.expanded and not pane.container.hiding then
|
||||||
|
pane.container:set_position(cx, cy)
|
||||||
|
pane.container:set_size(cw)
|
||||||
|
if not pane.container.visible then
|
||||||
|
pane.container:set_size(cw, ch)
|
||||||
|
pane.container:show_animated(true)
|
||||||
|
pane.tab:set_icon("-")
|
||||||
|
pane.container.hiding = false
|
||||||
|
end
|
||||||
|
elseif pane.container.visible and not pane.container.hiding then
|
||||||
|
pane.tab:set_icon("+")
|
||||||
|
pane.container.hiding = true
|
||||||
|
pane.container:hide_animated(true, false, {
|
||||||
|
on_complete = function()
|
||||||
|
pane.container.hiding = false
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
prev_pane = pane
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
---Here we draw the bottom line on each tab.
|
||||||
|
function FoldingBook:draw()
|
||||||
|
if not FoldingBook.super.draw(self) then return false end
|
||||||
|
|
||||||
|
for _, pane in ipairs(self.panes) do
|
||||||
|
local x = pane.tab.position.x
|
||||||
|
local y = pane.tab.position.y + pane.tab:get_height()
|
||||||
|
local w = self:get_width()
|
||||||
|
renderer.draw_rect(x, y, w, 2, style.selection)
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return FoldingBook
|
||||||
+287
@@ -0,0 +1,287 @@
|
|||||||
|
local core = require "core"
|
||||||
|
local common = require "core.common"
|
||||||
|
local Object = require "core.object"
|
||||||
|
local FontInfo = require "libraries.widget.fonts.info"
|
||||||
|
|
||||||
|
---@class widget.fonts.cache : core.object
|
||||||
|
---@overload fun():widget.fonts.cache
|
||||||
|
---@field fontinfo widget.fonts.info
|
||||||
|
---@field found integer
|
||||||
|
---@field found_monospaced integer
|
||||||
|
---@field building boolean
|
||||||
|
---@field monosppaced boolean
|
||||||
|
---@field searching_monospaced boolean
|
||||||
|
---@field fontdirs table<integer, string>
|
||||||
|
---@field fonts widget.fonts.data[]
|
||||||
|
local FontCache = Object:extend()
|
||||||
|
|
||||||
|
---Constructor
|
||||||
|
function FontCache:new()
|
||||||
|
self.fontinfo = FontInfo()
|
||||||
|
self.fontdirs = {}
|
||||||
|
self.fonts = {}
|
||||||
|
self.loaded_fonts = {}
|
||||||
|
self.found = 0
|
||||||
|
self.found_monospaced = 0
|
||||||
|
self.building = false
|
||||||
|
self.searching_monospaced = false
|
||||||
|
self.monospaced = false
|
||||||
|
|
||||||
|
table.insert(self.fontdirs, USERDIR .. "/fonts")
|
||||||
|
table.insert(self.fontdirs, DATADIR .. "/fonts")
|
||||||
|
|
||||||
|
if PLATFORM == "Windows" then
|
||||||
|
table.insert(self.fontdirs, HOME .. PATHSEP .. "AppData\\Local\\Microsoft\\Windows\\Fonts" )
|
||||||
|
table.insert(self.fontdirs, os.getenv("SYSTEMROOT") .. PATHSEP .. "Fonts" )
|
||||||
|
elseif PLATFORM == "Mac OS X" then
|
||||||
|
table.insert(self.fontdirs, HOME .. "/Library/Fonts")
|
||||||
|
table.insert(self.fontdirs, "/Library/Fonts")
|
||||||
|
table.insert(self.fontdirs, "/System/Library/Fonts")
|
||||||
|
else
|
||||||
|
table.insert(self.fontdirs, HOME .. "/.local/share/fonts")
|
||||||
|
table.insert(self.fontdirs, HOME .. "/.fonts")
|
||||||
|
table.insert(self.fontdirs, "/usr/local/share/fonts")
|
||||||
|
table.insert(self.fontdirs, "/usr/share/fonts")
|
||||||
|
end
|
||||||
|
|
||||||
|
if not self:load_cache() then
|
||||||
|
self:build()
|
||||||
|
elseif not self.monospaced then
|
||||||
|
self:verify_monospaced()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Check if the cache is already building.
|
||||||
|
---@return boolean building
|
||||||
|
function FontCache:is_building()
|
||||||
|
if self.building or self.searching_monospaced then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
---Build the font cache and save it.
|
||||||
|
---@return boolean started False if cache is already been built
|
||||||
|
function FontCache:build()
|
||||||
|
if self:is_building() then
|
||||||
|
core.log_quiet("The font cache is already been generated, please wait.")
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
self.found = 0
|
||||||
|
self.building = true
|
||||||
|
self.monospaced = false
|
||||||
|
self.loaded_fonts = {}
|
||||||
|
|
||||||
|
core.log_quiet("Generating font cache...")
|
||||||
|
local start_time = system.get_time()
|
||||||
|
|
||||||
|
local this = self
|
||||||
|
core.add_thread(function()
|
||||||
|
for _, dir in ipairs(this.fontdirs) do
|
||||||
|
this:scan_dir(dir)
|
||||||
|
end
|
||||||
|
this:save_cache()
|
||||||
|
this.building = false
|
||||||
|
this.loaded_fonts = {}
|
||||||
|
core.log_quiet(
|
||||||
|
"Font cache generated in %.1fs for %s fonts!",
|
||||||
|
system.get_time() - start_time, tostring(this.found)
|
||||||
|
)
|
||||||
|
self:verify_monospaced()
|
||||||
|
end)
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
---Clear current font cache and rebuild it.
|
||||||
|
---@return boolean started False if cache is already been built
|
||||||
|
function FontCache:rebuild()
|
||||||
|
if self:is_building() then
|
||||||
|
core.log_quiet("The font cache is already been generated, please wait.")
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local fontcache_file = USERDIR .. "/font_cache.lua"
|
||||||
|
local file = io.open(fontcache_file, "r")
|
||||||
|
|
||||||
|
if file ~= nil then
|
||||||
|
file:close()
|
||||||
|
os.remove(fontcache_file)
|
||||||
|
end
|
||||||
|
|
||||||
|
self.fonts = {}
|
||||||
|
self.loaded_fonts = {}
|
||||||
|
self.found = 0
|
||||||
|
self.found_monospaced = 0
|
||||||
|
|
||||||
|
return self:build()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Scan a directory for valid font files and load them into the cache.
|
||||||
|
---@param path string
|
||||||
|
---@param run_count? integer
|
||||||
|
function FontCache:scan_dir(path, run_count)
|
||||||
|
run_count = run_count or 1
|
||||||
|
local can_yield = coroutine.running()
|
||||||
|
local list = system.list_dir(path)
|
||||||
|
if list then
|
||||||
|
for _, name in pairs(list) do
|
||||||
|
if name:match("%.[tToO][tT][fFcC]$") and not self.loaded_fonts[name] then
|
||||||
|
-- prevent loading of duplicate files
|
||||||
|
self.loaded_fonts[name] = true
|
||||||
|
local font_path = path .. PATHSEP .. name
|
||||||
|
local read, errmsg = self.fontinfo:read(font_path)
|
||||||
|
|
||||||
|
if read then
|
||||||
|
local font_data
|
||||||
|
font_data, errmsg = self.fontinfo:get_data()
|
||||||
|
if font_data then
|
||||||
|
table.insert(self.fonts, font_data)
|
||||||
|
self.found = self.found + 1
|
||||||
|
else
|
||||||
|
io.stderr:write(
|
||||||
|
"Error: " .. path .. PATHSEP .. name .. "\n"
|
||||||
|
.. " " .. errmsg .. "\n"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
io.stderr:write(
|
||||||
|
"Error: " .. path .. PATHSEP .. name .. "\n"
|
||||||
|
.. " " .. errmsg .. "\n"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
if can_yield and run_count % 100 == 0 then
|
||||||
|
coroutine.yield()
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self:scan_dir(path .. PATHSEP .. name, run_count)
|
||||||
|
end
|
||||||
|
run_count = run_count + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Search and mark monospaced fonts on currently loaded cache and save it.
|
||||||
|
function FontCache:verify_monospaced()
|
||||||
|
if self:is_building() then
|
||||||
|
core.log_quiet("The monospaced verification is already running, please wait.")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
self.found_monospaced = 0
|
||||||
|
self.searching_monospaced = true
|
||||||
|
self.monospaced = false
|
||||||
|
|
||||||
|
core.log_quiet("Finding monospaced fonts...")
|
||||||
|
local start_time = system.get_time()
|
||||||
|
|
||||||
|
local this = self
|
||||||
|
core.add_thread(function()
|
||||||
|
for _, font in ipairs(this.fonts) do
|
||||||
|
if not font.monospace then
|
||||||
|
FontInfo.check_is_monospace(font)
|
||||||
|
end
|
||||||
|
if font.monospace then
|
||||||
|
this.found_monospaced = this.found_monospaced + 1
|
||||||
|
end
|
||||||
|
coroutine.yield()
|
||||||
|
end
|
||||||
|
this.monospaced = true
|
||||||
|
this:save_cache()
|
||||||
|
this.searching_monospaced = false
|
||||||
|
core.log_quiet(
|
||||||
|
"Found %s monospaced fonts in %.1fs!",
|
||||||
|
tostring(this.found_monospaced), system.get_time() - start_time
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Load font cache from persistent file for faster startup time.
|
||||||
|
function FontCache:load_cache()
|
||||||
|
local ok, t = pcall(dofile, USERDIR .. "/font_cache.lua")
|
||||||
|
if ok then
|
||||||
|
self.fonts = t.fonts
|
||||||
|
self.monospaced = t.monospaced
|
||||||
|
self.found = t.found
|
||||||
|
self.found_monospaced = t.found_monospaced
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
---Store current font cache to persistent file.
|
||||||
|
function FontCache:save_cache()
|
||||||
|
local fp = io.open(USERDIR .. "/font_cache.lua", "w")
|
||||||
|
if fp then
|
||||||
|
local output = "{\n"
|
||||||
|
.. "found = "..tostring(self.found)..",\n"
|
||||||
|
.. "found_monospaced = "..tostring(self.found_monospaced)..",\n"
|
||||||
|
.. "monospaced = "..tostring(self.monospaced)..",\n"
|
||||||
|
.. "[\"fonts\"] = "
|
||||||
|
.. common.serialize(
|
||||||
|
self.fonts,
|
||||||
|
{ pretty = true, escape = true, sort = true, initial_indent = 1 }
|
||||||
|
):gsub("^%s+", "")
|
||||||
|
.. "\n}\n"
|
||||||
|
fp:write("return ", output)
|
||||||
|
fp:close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Search for a font and return the best match.
|
||||||
|
---@param name string
|
||||||
|
---@param style? widget.fonts.style
|
||||||
|
---@param monospaced? boolean
|
||||||
|
---@return widget.fonts.data? font_data
|
||||||
|
---@return string? errmsg
|
||||||
|
function FontCache:search(name, style, monospaced)
|
||||||
|
if #self.fonts == 0 then
|
||||||
|
return nil, "the font cache needs to be rebuilt"
|
||||||
|
end
|
||||||
|
|
||||||
|
style = style or "regular"
|
||||||
|
name = name:ulower()
|
||||||
|
style = style:ulower()
|
||||||
|
|
||||||
|
if name == "monospace" then
|
||||||
|
name = "mono"
|
||||||
|
monospaced = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if not self.monospaced then monospaced = false end
|
||||||
|
|
||||||
|
---@type widget.fonts.data
|
||||||
|
local fontdata = nil
|
||||||
|
local prev_score = 0
|
||||||
|
|
||||||
|
for _, font in ipairs(self.fonts) do
|
||||||
|
if not monospaced or (monospaced and font.monospace) then
|
||||||
|
local score = system.fuzzy_match(
|
||||||
|
font.fullname:ulower(),
|
||||||
|
name .. " " .. style,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
if score ~= nil and (score > prev_score or prev_score == 0) then
|
||||||
|
fontdata = font
|
||||||
|
prev_score = score
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if fontdata then
|
||||||
|
local fontfile = io.open(fontdata.path, "r")
|
||||||
|
if not fontfile then
|
||||||
|
return nil, "found font file does not exists, cache is outdated"
|
||||||
|
else
|
||||||
|
fontfile:close()
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return nil, "no matching font found"
|
||||||
|
end
|
||||||
|
|
||||||
|
return fontdata
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return FontCache
|
||||||
+550
@@ -0,0 +1,550 @@
|
|||||||
|
-- Based on the code from:
|
||||||
|
-- https://gist.github.com/zr-tex8r/1969061a025fa4fc5486c9c28460f48e
|
||||||
|
|
||||||
|
local Object = require "core.object"
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Class Declarations
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
---@class widget.fonts.cdata : core.object
|
||||||
|
---@overload fun(data:string):widget.fonts.cdata
|
||||||
|
---@field private data string
|
||||||
|
---@field private position integer
|
||||||
|
local FontCDATA = Object:extend()
|
||||||
|
|
||||||
|
---@class widget.fonts.reader : core.object
|
||||||
|
---@overload fun(font_path?:string):widget.fonts.reader
|
||||||
|
---@field private file file*
|
||||||
|
---@field private path string
|
||||||
|
local FontReader = Object:extend()
|
||||||
|
|
||||||
|
---@class widget.fonts.data
|
||||||
|
---@field public path string
|
||||||
|
---@field public id number @Numerical id of the font
|
||||||
|
---@field public type '"ttc"' | '"ttf"' | '"otf"'
|
||||||
|
---@field public copyright string
|
||||||
|
---@field public family string
|
||||||
|
---@field public subfamily '"Regular"' | '"Bold"' | '"Italic"' | '"Bold Italic"'
|
||||||
|
---@field public fullname string
|
||||||
|
---@field public version string
|
||||||
|
---@field public psname string
|
||||||
|
---@field public url string
|
||||||
|
---@field public license string
|
||||||
|
---@field public tfamily string
|
||||||
|
---@field public tsubfamily '"Regular"' | '"Bold"' | '"Italic"' | '"Bold Italic"'
|
||||||
|
---@field public wwsfamily string
|
||||||
|
---@field public wwssubfamily string
|
||||||
|
---@field public monospace boolean
|
||||||
|
|
||||||
|
---@class widget.fonts.info : core.object
|
||||||
|
---@overload fun(font_path?:string):widget.fonts.info
|
||||||
|
---@field private reader widget.fonts.reader
|
||||||
|
---@field public path string @Path of the font file
|
||||||
|
---@field public data widget.fonts.data[] @Holds the metadata for each of the embedded fonts
|
||||||
|
local FontInfo = Object:extend()
|
||||||
|
|
||||||
|
---@alias widget.fonts.style
|
||||||
|
---|>'"regular"'
|
||||||
|
---| '"bold"'
|
||||||
|
---| '"italic"'
|
||||||
|
---| '"bold italic"'
|
||||||
|
---| '"thin"'
|
||||||
|
---| '"medium"'
|
||||||
|
---| '"light"'
|
||||||
|
---| '"black"'
|
||||||
|
---| '"condensed"'
|
||||||
|
---| '"oblique"'
|
||||||
|
---| '"bold oblique"'
|
||||||
|
---| '"extra nold"'
|
||||||
|
---| '"Extra bold italic"'
|
||||||
|
---| '"bold condensed"'
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- FontCDATA Implementation
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
function FontCDATA:new(data)
|
||||||
|
self.data = data
|
||||||
|
self.position = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function FontCDATA:__tostring()
|
||||||
|
return "cdata(pos="..self.position..")"
|
||||||
|
end
|
||||||
|
|
||||||
|
function FontCDATA:pos(p)
|
||||||
|
if not p then return self.position end
|
||||||
|
self.position = p
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function FontCDATA:unum(b)
|
||||||
|
local v, data = 0, self.data
|
||||||
|
assert(#data >= self.position + b, 11)
|
||||||
|
for _ = 1, b do
|
||||||
|
self.position = self.position + 1
|
||||||
|
v = v * 256 + data:byte(self.position)
|
||||||
|
end
|
||||||
|
return v
|
||||||
|
end
|
||||||
|
|
||||||
|
function FontCDATA:setunum(b, v)
|
||||||
|
local t, data = {}, self.data
|
||||||
|
t[1] = data:sub(1, self.position)
|
||||||
|
self.position = self.position + b
|
||||||
|
assert(#data >= self.position, 12)
|
||||||
|
t[b + 2] = data:sub(self.position + 1)
|
||||||
|
for i = 1, b do
|
||||||
|
t[b + 2 - i] = string.char(v % 256)
|
||||||
|
v = math.floor(v / 256)
|
||||||
|
end
|
||||||
|
self.data = table.concat(t, '')
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function FontCDATA:str(b)
|
||||||
|
local data = self.data
|
||||||
|
self.position = self.position + b
|
||||||
|
assert(#data >= self.position, 13)
|
||||||
|
return data:sub(self.position - b + 1, self.position)
|
||||||
|
end
|
||||||
|
|
||||||
|
function FontCDATA:setstr(s)
|
||||||
|
local t, data = {}, self.data
|
||||||
|
t[1], t[2] = data:sub(1, self.position), s
|
||||||
|
self.position = self.position + #s
|
||||||
|
assert(#data >= self.position, 14)
|
||||||
|
t[3] = data:sub(self.position + 1)
|
||||||
|
self.data = table.concat(t, '')
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function FontCDATA:ushort()
|
||||||
|
return self:unum(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
function FontCDATA:ulong()
|
||||||
|
return self:unum(4)
|
||||||
|
end
|
||||||
|
|
||||||
|
function FontCDATA:setulong(v)
|
||||||
|
return self:setunum(4, v)
|
||||||
|
end
|
||||||
|
|
||||||
|
function FontCDATA:ulongs(num)
|
||||||
|
local t = {}
|
||||||
|
for i = 1, num do
|
||||||
|
t[i] = self:unum(4)
|
||||||
|
end
|
||||||
|
return t
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- FontReader Implementation
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
function FontReader:new(font_path)
|
||||||
|
local file, errmsg = io.open(font_path, "rb")
|
||||||
|
assert(file, errmsg)
|
||||||
|
self.file = file
|
||||||
|
self.path = font_path
|
||||||
|
end
|
||||||
|
|
||||||
|
function FontReader:__gc()
|
||||||
|
if self.file then
|
||||||
|
self.file:close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function FontReader:__tostring()
|
||||||
|
return "reader("..self.path..")"
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param offset integer
|
||||||
|
---@param len integer
|
||||||
|
---@return widget.fonts.cdata?
|
||||||
|
---@return string|nil errmsg
|
||||||
|
function FontReader:cdata(offset, len)
|
||||||
|
local data, errmsg = self:read(offset, len)
|
||||||
|
if data then
|
||||||
|
return FontCDATA(data)
|
||||||
|
end
|
||||||
|
return nil, errmsg
|
||||||
|
end
|
||||||
|
|
||||||
|
function FontReader:read(offset, len)
|
||||||
|
self.file:seek("set", offset)
|
||||||
|
local data = self.file:read(len)
|
||||||
|
if data:len() ~= len then
|
||||||
|
return nil, "failed reading font data"
|
||||||
|
end
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
|
||||||
|
function FontReader:close()
|
||||||
|
self.file:close()
|
||||||
|
self.file = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- FontInfo Helper Functions
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- speeds up function lookups
|
||||||
|
local floor, ceil = math.floor, math.ceil
|
||||||
|
|
||||||
|
local function div(x, y)
|
||||||
|
return floor(x / y), x % y
|
||||||
|
end
|
||||||
|
|
||||||
|
local function utf16betoutf8(src)
|
||||||
|
local s, d = { tostring(src):byte(1, -1) }, {}
|
||||||
|
for i = 1, #s - 1, 2 do
|
||||||
|
local c = s[i] * 256 + s[i+1]
|
||||||
|
if c < 0x80 then d[#d+1] = c
|
||||||
|
elseif c < 0x800 then
|
||||||
|
local x, y = div(c, 0x40)
|
||||||
|
d[#d+1] = x + 0xC0; d[#d+1] = y + 0x80
|
||||||
|
elseif c < 0x10000 then
|
||||||
|
local x, y, z = div(c, 0x1000); y, z = div(y, 0x40)
|
||||||
|
d[#d+1] = x + 0xE0; d[#d+1] = y + 0x80; d[#d+1] = z + 0x80
|
||||||
|
else
|
||||||
|
assert(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return string.char(table.unpack(d))
|
||||||
|
end
|
||||||
|
|
||||||
|
local file_type = {
|
||||||
|
[0x74746366] = 'ttc',
|
||||||
|
[0x10000] = 'ttf',
|
||||||
|
[0x4F54544F] = 'otf',
|
||||||
|
[1008813135] = 'ttc'
|
||||||
|
}
|
||||||
|
|
||||||
|
---@param reader widget.fonts.reader
|
||||||
|
local function otf_offset(reader)
|
||||||
|
local cd, errmsg = reader:cdata(0, 12)
|
||||||
|
if not cd then
|
||||||
|
return nil, errmsg
|
||||||
|
end
|
||||||
|
local tag = cd:ulong()
|
||||||
|
local ftype = file_type[tag];
|
||||||
|
if ftype == 'ttc' then
|
||||||
|
local ver = cd:ulong();
|
||||||
|
local num = cd:ulong();
|
||||||
|
cd, errmsg = reader:cdata(12, 4 * num)
|
||||||
|
if not cd then
|
||||||
|
return nil, errmsg
|
||||||
|
end
|
||||||
|
local res = cd:ulongs(num);
|
||||||
|
return res
|
||||||
|
elseif ftype == 'otf' or ftype == 'ttf' then
|
||||||
|
return { 0 }
|
||||||
|
else
|
||||||
|
return nil, string.format("unknown file tag: %s", tag)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param reader widget.fonts.reader
|
||||||
|
---@param fofs integer
|
||||||
|
---@param ntbl integer
|
||||||
|
local function otf_name_table(reader, fofs, ntbl)
|
||||||
|
local cd_d = reader:cdata(fofs + 12, 16 * ntbl)
|
||||||
|
if not cd_d then
|
||||||
|
return nil, "error reading names table"
|
||||||
|
end
|
||||||
|
for _ = 1, ntbl do
|
||||||
|
local t = {-- tag, csum, ofs, len
|
||||||
|
cd_d:str(4), cd_d:ulong(), cd_d:ulong(), cd_d:ulong()
|
||||||
|
}
|
||||||
|
if t[1] == 'name' then
|
||||||
|
return reader:cdata(t[3], ceil(t[4] / 4) * 4)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil, "name table is missing"
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param cdata widget.fonts.cdata
|
||||||
|
local function otf_name_records(cdata)
|
||||||
|
local nfmt, nnum, nofs = cdata:ushort(), cdata:ushort(), cdata:ushort()
|
||||||
|
assert(nfmt == 0, string.format("unsupported name table format: %s", nfmt))
|
||||||
|
local nr = {}
|
||||||
|
for i = 1, nnum do
|
||||||
|
nr[i] = { -- pid, eid, langid, nameid, len, ofs
|
||||||
|
cdata:ushort(), cdata:ushort(), cdata:ushort(),
|
||||||
|
cdata:ushort(), cdata:ushort(), cdata:ushort() + nofs
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return nr
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param cdata widget.fonts.cdata
|
||||||
|
local function otf_name(cdata, nr, nameid)
|
||||||
|
local function seek(pid, eid, lid)
|
||||||
|
for i = 1, #nr do
|
||||||
|
local t = nr[i]
|
||||||
|
local ok = (t[4] == nameid and t[1] == pid and t[2] == eid and
|
||||||
|
t[3] == lid)
|
||||||
|
if ok then return t end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local rec = seek(3, 1, 0x409)
|
||||||
|
or seek(3, 10, 0x409)
|
||||||
|
or seek(1, 0, 0) or seek(0, 3, 0)
|
||||||
|
or seek(0, 4, 0) or seek(0, 6, 0)
|
||||||
|
|
||||||
|
if not rec then return '' end
|
||||||
|
local s = cdata:pos(rec[6]):str(rec[5])
|
||||||
|
return (rec[1] == 3) and utf16betoutf8(s) or s
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param reader widget.fonts.reader
|
||||||
|
local function otf_list(reader, fid, fofs)
|
||||||
|
local cd_fh, errmsg = reader:cdata(fofs, 12)
|
||||||
|
if not cd_fh then
|
||||||
|
return nil, errmsg
|
||||||
|
end
|
||||||
|
|
||||||
|
local tag = cd_fh:ulong()
|
||||||
|
local ntbl = cd_fh:ushort()
|
||||||
|
|
||||||
|
local cd_n = nil
|
||||||
|
cd_n, errmsg = otf_name_table(reader, fofs, ntbl)
|
||||||
|
if not cd_n then
|
||||||
|
return nil, errmsg
|
||||||
|
end
|
||||||
|
|
||||||
|
local ext = { id = fid; type = file_type[tag] or '' }
|
||||||
|
|
||||||
|
local nr = nil
|
||||||
|
nr, errmsg = otf_name_records(cd_n)
|
||||||
|
if not nr then
|
||||||
|
return nil, errmsg
|
||||||
|
end
|
||||||
|
|
||||||
|
local output = {
|
||||||
|
id = ext.id,
|
||||||
|
type = ext.type,
|
||||||
|
copyright = otf_name(cd_n, nr, 0),
|
||||||
|
family = otf_name(cd_n, nr, 1),
|
||||||
|
subfamily = otf_name(cd_n, nr, 2),
|
||||||
|
fullname = otf_name(cd_n, nr, 4),
|
||||||
|
version = otf_name(cd_n, nr, 5),
|
||||||
|
psname = otf_name(cd_n, nr, 6),
|
||||||
|
url = otf_name(cd_n, nr, 11),
|
||||||
|
license = otf_name(cd_n, nr, 13),
|
||||||
|
tfamily = otf_name(cd_n, nr, 16),
|
||||||
|
tsubfamily = otf_name(cd_n, nr, 17),
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- FontInfo Implementation
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
---Helper function to check and update a font monospace attribute.
|
||||||
|
---@param font_data widget.fonts.data
|
||||||
|
---@return boolean checked
|
||||||
|
---@return string? errmsg
|
||||||
|
function FontInfo.check_is_monospace(font_data)
|
||||||
|
if font_data then
|
||||||
|
local loaded, fontren = pcall(renderer.font.load, font_data.path, 8, {})
|
||||||
|
if not loaded then
|
||||||
|
return false, "could not load font"
|
||||||
|
else
|
||||||
|
if fontren:get_width("|") == fontren:get_width("W") then
|
||||||
|
font_data.monospace = true
|
||||||
|
else
|
||||||
|
font_data.monospace = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
---Constructor
|
||||||
|
---@param font_path? string
|
||||||
|
function FontInfo:new(font_path)
|
||||||
|
if type(font_path) == "string" then
|
||||||
|
self:read(font_path)
|
||||||
|
else
|
||||||
|
self.data = {}
|
||||||
|
self.path = ""
|
||||||
|
self.last_error = "no font given"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function fontinfo_read_native(self, font_path)
|
||||||
|
---@type widget.fonts.data
|
||||||
|
local font
|
||||||
|
---@type string?
|
||||||
|
local errmsg
|
||||||
|
|
||||||
|
---@diagnostic disable-next-line
|
||||||
|
font, errmsg = renderer.font.get_metadata(font_path)
|
||||||
|
|
||||||
|
if not font then
|
||||||
|
self.last_error = errmsg
|
||||||
|
return font, errmsg
|
||||||
|
end
|
||||||
|
|
||||||
|
local add = true
|
||||||
|
local family = nil
|
||||||
|
if font.tfamily then
|
||||||
|
family = font.tfamily
|
||||||
|
elseif font.family then
|
||||||
|
family = font.family
|
||||||
|
end
|
||||||
|
|
||||||
|
local subfamily = nil
|
||||||
|
if font.tsubfamily then
|
||||||
|
subfamily = font.tsubfamily -- sometimes tsubfamily includes more styles
|
||||||
|
elseif font.subfamily then
|
||||||
|
subfamily = font.subfamily
|
||||||
|
end
|
||||||
|
|
||||||
|
-- fix font meta data or discard if empty
|
||||||
|
if family and subfamily then
|
||||||
|
font.fullname = family .. " " .. subfamily
|
||||||
|
elseif font.fullname and family and not font.fullname:ufind(family, 1, true) then
|
||||||
|
font.fullname = font.fullname .. " " .. family
|
||||||
|
elseif not font.fullname and family then
|
||||||
|
font.fullname = family
|
||||||
|
else
|
||||||
|
self.last_error = "font metadata is empty"
|
||||||
|
add = false
|
||||||
|
end
|
||||||
|
|
||||||
|
if add then
|
||||||
|
table.insert(self.data, font)
|
||||||
|
else
|
||||||
|
return nil, self.last_error
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function fontinfo_read_nonnative(self, font_path)
|
||||||
|
self.reader = FontReader(font_path)
|
||||||
|
|
||||||
|
local tofs, errmsg = otf_offset(self.reader)
|
||||||
|
|
||||||
|
if not tofs then
|
||||||
|
self.last_error = errmsg
|
||||||
|
return nil, errmsg
|
||||||
|
end
|
||||||
|
|
||||||
|
local data = nil
|
||||||
|
for i = 1, #tofs do
|
||||||
|
data, errmsg = otf_list(self.reader, i - 1, tofs[i])
|
||||||
|
if data then
|
||||||
|
table.insert(self.data, data)
|
||||||
|
else
|
||||||
|
self.last_error = errmsg
|
||||||
|
return nil, errmsg
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.data[1] then
|
||||||
|
local font = self.data[1]
|
||||||
|
|
||||||
|
local family = nil
|
||||||
|
if font.tfamily ~= "" then
|
||||||
|
family = font.tfamily
|
||||||
|
elseif font.family ~= "" then
|
||||||
|
family = font.family
|
||||||
|
end
|
||||||
|
|
||||||
|
local subfamily = nil
|
||||||
|
if font.tsubfamily ~= "" then
|
||||||
|
subfamily = font.tsubfamily -- sometimes tsubfamily includes more styles
|
||||||
|
elseif font.subfamily ~= "" then
|
||||||
|
subfamily = font.subfamily
|
||||||
|
end
|
||||||
|
|
||||||
|
-- fix font meta data or discard if empty
|
||||||
|
if family and subfamily then
|
||||||
|
font.fullname = family .. " " .. subfamily
|
||||||
|
elseif font.fullname ~= "" and family and not font.fullname:ufind(family, 1, true) then
|
||||||
|
font.fullname = font.fullname .. " " .. family
|
||||||
|
elseif font.fullname == "" and family then
|
||||||
|
font.fullname = family
|
||||||
|
else
|
||||||
|
self.data = {}
|
||||||
|
self.last_error = "font metadata is empty"
|
||||||
|
return nil, self.last_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self.reader:close()
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
---Open a font file and read its metadata.
|
||||||
|
---@param font_path string
|
||||||
|
---@return widget.fonts.info?
|
||||||
|
---@return string|nil errmsg
|
||||||
|
function FontInfo:read(font_path)
|
||||||
|
self.data = {}
|
||||||
|
self.path = font_path
|
||||||
|
|
||||||
|
local read, errmsg
|
||||||
|
|
||||||
|
---@diagnostic disable-next-line
|
||||||
|
if type(renderer.font.get_metadata) == "function" then
|
||||||
|
read, errmsg = fontinfo_read_native(self, font_path)
|
||||||
|
else
|
||||||
|
read, errmsg = fontinfo_read_nonnative(self, font_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
if not read then
|
||||||
|
return read, errmsg
|
||||||
|
end
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the amount of collections on the font file.
|
||||||
|
---@return integer
|
||||||
|
function FontInfo:embedded_fonts_count()
|
||||||
|
return #self.data
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the metadata of a previously read font file without
|
||||||
|
---copyright and license information which can be long.
|
||||||
|
---@param idx? integer Optional position of the embedded font
|
||||||
|
---@return widget.fonts.data?
|
||||||
|
---@return string|nil errmsg
|
||||||
|
function FontInfo:get_data(idx)
|
||||||
|
idx = idx or 1
|
||||||
|
local data = {}
|
||||||
|
|
||||||
|
if #self.data > 0 and self.data[idx] then
|
||||||
|
data = self.data[idx]
|
||||||
|
else
|
||||||
|
return nil, self.last_error
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
path = self.path,
|
||||||
|
id = data.id,
|
||||||
|
type = data.type,
|
||||||
|
family = data.family,
|
||||||
|
subfamily = data.subfamily,
|
||||||
|
fullname = data.fullname,
|
||||||
|
version = data.version,
|
||||||
|
psname = data.psname,
|
||||||
|
url = data.url,
|
||||||
|
tfamily = data.tfamily,
|
||||||
|
tsubfamily = data.tsubfamily,
|
||||||
|
wwsfamily = data.wwsfamily,
|
||||||
|
wwssubfamily = data.wwssubfamily,
|
||||||
|
monospace = data.monospace or false
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return FontInfo
|
||||||
+230
@@ -0,0 +1,230 @@
|
|||||||
|
local core = require "core"
|
||||||
|
local common = require "core.common"
|
||||||
|
local style = require "core.style"
|
||||||
|
local FontCache = require "libraries.widget.fonts.cache"
|
||||||
|
local StatusView = require "core.statusview"
|
||||||
|
|
||||||
|
---@class widget.fonts
|
||||||
|
local Fonts = {}
|
||||||
|
|
||||||
|
---@type widget.fonts.cache | nil
|
||||||
|
local fontcache = nil
|
||||||
|
|
||||||
|
---@type table<integer, string> | nil
|
||||||
|
local fonts = nil
|
||||||
|
|
||||||
|
---Last time the status view item was rendered
|
||||||
|
local last_statusview_render = 0
|
||||||
|
|
||||||
|
---The amount of fonts matching the user query
|
||||||
|
local matching_fonts = 0
|
||||||
|
|
||||||
|
---Flag that indicates if command view font picker is for monospaced
|
||||||
|
local pick_monospaced = false
|
||||||
|
|
||||||
|
---Generate the list of fonts displayed on the CommandView.
|
||||||
|
---@param monospaced? boolean Only display fonts detected as monospaced.
|
||||||
|
local function generate_fonts(monospaced)
|
||||||
|
if fontcache then
|
||||||
|
if fontcache.building then monospaced = false end
|
||||||
|
fonts = {}
|
||||||
|
for idx, f in ipairs(fontcache.fonts) do
|
||||||
|
if not monospaced or (monospaced and f.monospace) then
|
||||||
|
table.insert(fonts, f.fullname .. "||" .. idx)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Helper function to split a string by a given delimeter.
|
||||||
|
local function split(s, delimeter, delimeter_pattern)
|
||||||
|
if not delimeter_pattern then
|
||||||
|
delimeter_pattern = delimeter
|
||||||
|
end
|
||||||
|
|
||||||
|
local result = {};
|
||||||
|
for match in (s..delimeter):gmatch("(.-)"..delimeter_pattern) do
|
||||||
|
table.insert(result, match);
|
||||||
|
end
|
||||||
|
return result;
|
||||||
|
end
|
||||||
|
|
||||||
|
local already_cleaning = false
|
||||||
|
|
||||||
|
---Clean the generated font cache used on command view to free some ram
|
||||||
|
local function clean_fonts_cache()
|
||||||
|
if not fontcache or already_cleaning then return end
|
||||||
|
if not fontcache.building and not fontcache.searching_monospaced then
|
||||||
|
fontcache = nil
|
||||||
|
fonts = nil
|
||||||
|
collectgarbage "collect"
|
||||||
|
else
|
||||||
|
already_cleaning = true
|
||||||
|
core.add_thread(function()
|
||||||
|
while fontcache.building or fontcache.searching_monospaced do
|
||||||
|
coroutine.yield(1)
|
||||||
|
end
|
||||||
|
if
|
||||||
|
core.active_view ~= core.command_view
|
||||||
|
or
|
||||||
|
(
|
||||||
|
core.command_view.label ~= "Select Font: "
|
||||||
|
and
|
||||||
|
core.command_view.label ~= "List only monospaced fonts?: "
|
||||||
|
)
|
||||||
|
then
|
||||||
|
fontcache = nil
|
||||||
|
fonts = nil
|
||||||
|
collectgarbage "collect"
|
||||||
|
already_cleaning = false
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Launch the commandview and let the user select a font.
|
||||||
|
---@param callback fun(name:string, path:string)
|
||||||
|
---@param monospaced boolean
|
||||||
|
function Fonts.show_picker(callback, monospaced)
|
||||||
|
if not fontcache then fontcache = FontCache() end
|
||||||
|
|
||||||
|
pick_monospaced = monospaced
|
||||||
|
|
||||||
|
if not fontcache.building and (not monospaced or fontcache.monospaced) then
|
||||||
|
generate_fonts(monospaced)
|
||||||
|
else
|
||||||
|
core.add_thread(function()
|
||||||
|
while
|
||||||
|
(fontcache.building or (monospaced and not fontcache.monospaced))
|
||||||
|
and
|
||||||
|
core.active_view == core.command_view
|
||||||
|
and
|
||||||
|
core.command_view.label == "Select Font: "
|
||||||
|
do
|
||||||
|
core.command_view:update_suggestions()
|
||||||
|
coroutine.yield(2)
|
||||||
|
end
|
||||||
|
generate_fonts(monospaced)
|
||||||
|
core.command_view:update_suggestions()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
last_statusview_render = system.get_time()
|
||||||
|
|
||||||
|
core.command_view:enter("Select Font", {
|
||||||
|
submit = function(text, item)
|
||||||
|
callback(item.text, item.info)
|
||||||
|
clean_fonts_cache()
|
||||||
|
end,
|
||||||
|
suggest = function(text)
|
||||||
|
if fontcache.building or (monospaced and fontcache.searching_monospaced) then
|
||||||
|
generate_fonts(monospaced)
|
||||||
|
end
|
||||||
|
local res = common.fuzzy_match(fonts, text)
|
||||||
|
matching_fonts = #res
|
||||||
|
for i, name in ipairs(res) do
|
||||||
|
local font_info = split(name, "||")
|
||||||
|
local id = tonumber(font_info[2])
|
||||||
|
local font_data = fontcache.fonts[id]
|
||||||
|
res[i] = {
|
||||||
|
text = font_data.fullname,
|
||||||
|
info = font_data.path,
|
||||||
|
id = id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
end,
|
||||||
|
cancel = function()
|
||||||
|
clean_fonts_cache()
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
---Same as `show_picker()` but asks the user if he wants a monospaced font.
|
||||||
|
---@param callback fun(name:string, path:string)
|
||||||
|
function Fonts.show_picker_ask_monospace(callback)
|
||||||
|
if not fontcache then fontcache = FontCache() end
|
||||||
|
|
||||||
|
core.command_view:enter("List only monospaced fonts?", {
|
||||||
|
submit = function(text, item)
|
||||||
|
Fonts.show_picker(callback, item.mono)
|
||||||
|
end,
|
||||||
|
suggest = function(text)
|
||||||
|
local res = common.fuzzy_match({"Yes", "No"}, text)
|
||||||
|
for i, name in ipairs(res) do
|
||||||
|
res[i] = {
|
||||||
|
text = name,
|
||||||
|
mono = text == "Yes" and true or false
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
end,
|
||||||
|
cancel = function()
|
||||||
|
clean_fonts_cache()
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
---Check if the font cache is been built.
|
||||||
|
---@return boolean building
|
||||||
|
function Fonts.cache_is_building()
|
||||||
|
if not fontcache then return false end
|
||||||
|
return fontcache:is_building()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Remove current fonts cache file and regenerates a fresh one.
|
||||||
|
---@return boolean started False if cache is already been built
|
||||||
|
function Fonts.clean_cache()
|
||||||
|
if not fontcache then fontcache = FontCache() end
|
||||||
|
return fontcache:rebuild()
|
||||||
|
end
|
||||||
|
|
||||||
|
core.status_view:add_item({
|
||||||
|
predicate = function()
|
||||||
|
return core.active_view == core.command_view
|
||||||
|
and core.command_view.label == "Select Font: "
|
||||||
|
end,
|
||||||
|
name = "widget:font-select",
|
||||||
|
alignment = StatusView.Item.LEFT,
|
||||||
|
get_item = function()
|
||||||
|
local found = 0
|
||||||
|
local dots, status = "", ""
|
||||||
|
if fontcache then
|
||||||
|
if fontcache.building or fontcache.searching_monospaced then
|
||||||
|
dots = "."
|
||||||
|
if system.get_time() - last_statusview_render >= 3 then
|
||||||
|
last_statusview_render = system.get_time()
|
||||||
|
elseif system.get_time() - last_statusview_render >= 2 then
|
||||||
|
dots = "..."
|
||||||
|
elseif system.get_time() - last_statusview_render >= 1 then
|
||||||
|
dots = ".."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if fontcache.building then
|
||||||
|
status = " | searching system fonts" .. dots
|
||||||
|
elseif fontcache.searching_monospaced then
|
||||||
|
status = " | detecting monospaced fonts" .. dots
|
||||||
|
end
|
||||||
|
|
||||||
|
if fontcache.building or not pick_monospaced then
|
||||||
|
found = fontcache.found
|
||||||
|
else
|
||||||
|
found = fontcache.found_monospaced
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
style.text,
|
||||||
|
style.font,
|
||||||
|
"Matches: "
|
||||||
|
.. tostring(matching_fonts)
|
||||||
|
.. " / "
|
||||||
|
.. tostring(found)
|
||||||
|
.. status
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
position = 1
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
return Fonts
|
||||||
+223
@@ -0,0 +1,223 @@
|
|||||||
|
--
|
||||||
|
-- FontsList Widget.
|
||||||
|
-- @copyright Jefferson Gonzalez
|
||||||
|
-- @license MIT
|
||||||
|
--
|
||||||
|
local style = require "core.style"
|
||||||
|
local Widget = require "libraries.widget"
|
||||||
|
local Button = require "libraries.widget.button"
|
||||||
|
local ListBox = require "libraries.widget.listbox"
|
||||||
|
local FontDialog = require "libraries.widget.fontdialog"
|
||||||
|
local MessageBox = require "libraries.widget.messagebox"
|
||||||
|
|
||||||
|
---@class widget.fontslist.font : widget
|
||||||
|
---@field name string
|
||||||
|
---@field path string
|
||||||
|
|
||||||
|
---@class widget.fontslist : widget
|
||||||
|
---@overload fun(parent?:widget):widget.fontslist
|
||||||
|
---@field list widget.listbox
|
||||||
|
---@field add widget.button
|
||||||
|
---@field remove widget.button
|
||||||
|
---@field up widget.button
|
||||||
|
---@field down widget.button
|
||||||
|
---@field options renderer.fontoptions
|
||||||
|
---@field private dialog boolean
|
||||||
|
local FontsList = Widget:extend()
|
||||||
|
|
||||||
|
---Constructor
|
||||||
|
---@param parent widget
|
||||||
|
function FontsList:new(parent)
|
||||||
|
FontsList.super.new(self, parent)
|
||||||
|
|
||||||
|
self.type_name = "widget.fontslist"
|
||||||
|
|
||||||
|
self.border.width = 0
|
||||||
|
|
||||||
|
self.dialog = false
|
||||||
|
|
||||||
|
self.options = {}
|
||||||
|
|
||||||
|
self.list = ListBox(self)
|
||||||
|
|
||||||
|
local this = self
|
||||||
|
|
||||||
|
function self.list:on_mouse_pressed(button, x, y, clicks)
|
||||||
|
if not ListBox.on_mouse_pressed(self, button, x, y, clicks) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if clicks == 2 and not this.dialog then
|
||||||
|
this.dialog = true
|
||||||
|
local selected = this.list:get_selected()
|
||||||
|
local fontdata = this.list:get_row_data(selected)
|
||||||
|
---@type widget.inputdialog
|
||||||
|
local font = FontDialog(fontdata, this:get_options())
|
||||||
|
function font:on_save(fontdata, options)
|
||||||
|
this:set_options(options)
|
||||||
|
this:edit_font(selected, fontdata)
|
||||||
|
end
|
||||||
|
function font:on_close()
|
||||||
|
FontDialog.on_close(self)
|
||||||
|
self:destroy()
|
||||||
|
this.dialog = false
|
||||||
|
end
|
||||||
|
font:show()
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
self.add = Button(self, "Add")
|
||||||
|
self.add:set_icon("B")
|
||||||
|
function self.add:on_click()
|
||||||
|
if #this.list.rows > 9 then
|
||||||
|
MessageBox.error("Max Fonts Reached", "Only a maximum of ten fonts can be added.")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not this.dialog then
|
||||||
|
this.dialog = true
|
||||||
|
---@type widget.inputdialog
|
||||||
|
local font = FontDialog(nil, this:get_options())
|
||||||
|
function font:on_save(fontdata, options)
|
||||||
|
this:set_options(options)
|
||||||
|
this:add_font(fontdata)
|
||||||
|
end
|
||||||
|
function font:on_close()
|
||||||
|
FontDialog.on_close(self)
|
||||||
|
self:destroy()
|
||||||
|
this.dialog = false
|
||||||
|
end
|
||||||
|
font:show()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self.remove = Button(self, "Remove")
|
||||||
|
self.remove:set_icon("C")
|
||||||
|
function self.remove:on_click()
|
||||||
|
local selected = this.list:get_selected()
|
||||||
|
if selected then
|
||||||
|
if #this.list.rows > 1 then
|
||||||
|
this:remove_font(selected)
|
||||||
|
else
|
||||||
|
MessageBox.error("Font required", "A minimum of one font is needed")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
MessageBox.error("No font selected", "Please select a font to remove")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self.up = Button(self, "")
|
||||||
|
self.up:set_icon("<")
|
||||||
|
self.up:set_tooltip("increase font priority")
|
||||||
|
function self.up:on_click()
|
||||||
|
local selected = this.list:get_selected()
|
||||||
|
if selected then
|
||||||
|
this.list:move_row_up(selected)
|
||||||
|
this:on_change()
|
||||||
|
else
|
||||||
|
MessageBox.error("No font selected", "Please select a font to move")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self.down = Button(self, "")
|
||||||
|
self.down:set_icon(">")
|
||||||
|
self.down:set_tooltip("decrease font priority")
|
||||||
|
function self.down:on_click()
|
||||||
|
local selected = this.list:get_selected()
|
||||||
|
if selected then
|
||||||
|
this.list:move_row_down(selected)
|
||||||
|
this:on_change()
|
||||||
|
else
|
||||||
|
MessageBox.error("No font selected", "Please select a font to move")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Add a new font into the list.
|
||||||
|
---@param font widget.fontslist.font
|
||||||
|
function FontsList:add_font(font)
|
||||||
|
self.list:add_row({font.name}, font)
|
||||||
|
self.list:set_visible_rows()
|
||||||
|
self:on_change()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Edit an existing font on the list.
|
||||||
|
---@param idx integer
|
||||||
|
---@param font widget.fontslist.font
|
||||||
|
function FontsList:edit_font(idx, font)
|
||||||
|
self.list:set_row(idx, {font.name})
|
||||||
|
self.list:set_row_data(idx, font)
|
||||||
|
self:on_change()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Remove the given font from the list.
|
||||||
|
---@param idx integer
|
||||||
|
function FontsList:remove_font(idx)
|
||||||
|
self.list:remove_row(idx)
|
||||||
|
self:on_change()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Return the fonts from the list.
|
||||||
|
---@return table<integer, string>
|
||||||
|
function FontsList:get_fonts()
|
||||||
|
local output = {}
|
||||||
|
local count = #self.list.rows
|
||||||
|
for i=1, count, 1 do
|
||||||
|
table.insert(output, self.list:get_row_data(i))
|
||||||
|
end
|
||||||
|
return output
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set the global options for the fonts group
|
||||||
|
---@param options renderer.fontoptions
|
||||||
|
function FontsList:set_options(options)
|
||||||
|
self.options = options
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the global options for the font group
|
||||||
|
---@return renderer.fontoptions
|
||||||
|
function FontsList:get_options()
|
||||||
|
return self.options
|
||||||
|
end
|
||||||
|
|
||||||
|
function FontsList:update()
|
||||||
|
if not FontsList.super.update(self) then return false end
|
||||||
|
|
||||||
|
if self.size.x == 0 then
|
||||||
|
self.size.x = self.add:get_width()
|
||||||
|
+ (style.padding.x / 2) + self.remove:get_width()
|
||||||
|
+ (style.padding.x / 2) + self.up:get_width()
|
||||||
|
+ (style.padding.x / 2) + self.down:get_width() + (50 * SCALE)
|
||||||
|
self.size.y = self.add:get_height() + (style.padding.y * 2) + 100
|
||||||
|
end
|
||||||
|
|
||||||
|
self.list:set_position(0, 0)
|
||||||
|
|
||||||
|
self.list:set_size(
|
||||||
|
self.size.x,
|
||||||
|
self.size.y - self.add:get_height() - (style.padding.y * 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.add:set_position(0, self.list:get_bottom() + style.padding.y)
|
||||||
|
|
||||||
|
self.remove:set_position(
|
||||||
|
self.add:get_right() + (style.padding.x / 2),
|
||||||
|
self.list:get_bottom() + style.padding.y
|
||||||
|
)
|
||||||
|
|
||||||
|
self.up:set_position(
|
||||||
|
self.remove:get_right() + (style.padding.x / 2),
|
||||||
|
self.list:get_bottom() + style.padding.y
|
||||||
|
)
|
||||||
|
|
||||||
|
self.down:set_position(
|
||||||
|
self.up:get_right() + (style.padding.x / 2),
|
||||||
|
self.list:get_bottom() + style.padding.y
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return FontsList
|
||||||
+1544
File diff suppressed because it is too large
Load Diff
+88
@@ -0,0 +1,88 @@
|
|||||||
|
--
|
||||||
|
-- Input Dialog Widget.
|
||||||
|
-- @copyright Jefferson Gonzalez
|
||||||
|
-- @license MIT
|
||||||
|
--
|
||||||
|
|
||||||
|
local style = require "core.style"
|
||||||
|
local Button = require "libraries.widget.button"
|
||||||
|
local Dialog = require "libraries.widget.dialog"
|
||||||
|
local Label = require "libraries.widget.label"
|
||||||
|
local TextBox = require "libraries.widget.textbox"
|
||||||
|
|
||||||
|
---@class widget.inputdialog : widget.dialog
|
||||||
|
---@overload fun(title?:string, message?:string, text?:string):widget.inputdialog
|
||||||
|
---@field super widget.dialog
|
||||||
|
---@field message widget.label
|
||||||
|
---@field text widget.textbox
|
||||||
|
---@field save widget.button
|
||||||
|
---@field cancel widget.button
|
||||||
|
local InputDialog = Dialog:extend()
|
||||||
|
|
||||||
|
---Constructor
|
||||||
|
---@param title string
|
||||||
|
---@param message string
|
||||||
|
---@param text string
|
||||||
|
function InputDialog:new(title, message, text)
|
||||||
|
InputDialog.super.new(self, title or "Enter Value")
|
||||||
|
|
||||||
|
self.type_name = "widget.inputdialog"
|
||||||
|
|
||||||
|
self.message = Label(self.panel, message)
|
||||||
|
self.text = TextBox(self.panel, text or "")
|
||||||
|
|
||||||
|
local this = self
|
||||||
|
|
||||||
|
self.save = Button(self.panel, "Save")
|
||||||
|
self.save:set_icon("S")
|
||||||
|
function self.save:on_click()
|
||||||
|
this:on_save(this.text:get_text())
|
||||||
|
this:on_close()
|
||||||
|
end
|
||||||
|
|
||||||
|
self.cancel = Button(self.panel, "Cancel")
|
||||||
|
self.cancel:set_icon("C")
|
||||||
|
function self.cancel:on_click()
|
||||||
|
this:on_close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Called when the user clicks on save
|
||||||
|
---@param value string
|
||||||
|
function InputDialog:on_save(value) end
|
||||||
|
|
||||||
|
function InputDialog:update()
|
||||||
|
if not InputDialog.super.update(self) then return false end
|
||||||
|
|
||||||
|
self.message:set_position(style.padding.x/2, 0)
|
||||||
|
self.text:set_position(style.padding.x/2, self.message:get_bottom() + style.padding.y)
|
||||||
|
|
||||||
|
self.save:set_position(
|
||||||
|
style.padding.x/2,
|
||||||
|
self.text:get_bottom() + style.padding.y
|
||||||
|
)
|
||||||
|
self.cancel:set_position(
|
||||||
|
self.save:get_right() + style.padding.x,
|
||||||
|
self.text:get_bottom() + style.padding.y
|
||||||
|
)
|
||||||
|
|
||||||
|
self.panel.size.x = self.panel:get_real_width() + style.padding.x
|
||||||
|
self.panel.size.y = self.panel:get_real_height()
|
||||||
|
self.size.x = self:get_real_width() - (style.padding.x / 2)
|
||||||
|
self.size.y = self:get_real_height() + (style.padding.y / 2)
|
||||||
|
|
||||||
|
self.text:set_size(
|
||||||
|
self.size.x - style.padding.x,
|
||||||
|
self.text:get_real_height()
|
||||||
|
)
|
||||||
|
|
||||||
|
self.close:set_position(
|
||||||
|
self.size.x - self.close.size.x - (style.padding.x / 2),
|
||||||
|
style.padding.y / 2
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return InputDialog
|
||||||
+162
@@ -0,0 +1,162 @@
|
|||||||
|
--
|
||||||
|
-- ItemsList Widget.
|
||||||
|
-- @copyright Jefferson Gonzalez
|
||||||
|
-- @license MIT
|
||||||
|
--
|
||||||
|
local style = require "core.style"
|
||||||
|
local Widget = require "libraries.widget"
|
||||||
|
local Button = require "libraries.widget.button"
|
||||||
|
local ListBox = require "libraries.widget.listbox"
|
||||||
|
local InputDialog = require "libraries.widget.inputdialog"
|
||||||
|
local MessageBox = require "libraries.widget.messagebox"
|
||||||
|
|
||||||
|
---@class widget.itemslist : widget
|
||||||
|
---@overload fun(parent:widget?):widget.itemslist
|
||||||
|
---@field list widget.listbox
|
||||||
|
---@field add widget.button
|
||||||
|
---@field remove widget.button
|
||||||
|
local ItemsList = Widget:extend()
|
||||||
|
|
||||||
|
---Constructor
|
||||||
|
---@param parent widget
|
||||||
|
function ItemsList:new(parent)
|
||||||
|
ItemsList.super.new(self, parent)
|
||||||
|
|
||||||
|
self.type_name = "widget.itemslist"
|
||||||
|
|
||||||
|
self.border.width = 0
|
||||||
|
|
||||||
|
self.dialog = false
|
||||||
|
|
||||||
|
self.list = ListBox(self)
|
||||||
|
|
||||||
|
local this = self
|
||||||
|
|
||||||
|
function self.list:on_mouse_pressed(button, x, y, clicks)
|
||||||
|
if not ListBox.on_mouse_pressed(self, button, x, y, clicks) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if clicks == 2 and not this.dialog then
|
||||||
|
this.dialog = true
|
||||||
|
local selected = this.list:get_selected()
|
||||||
|
local selvalue = this.list:get_row_text(selected)
|
||||||
|
---@type widget.inputdialog
|
||||||
|
local input = InputDialog("Edit Item", "Enter the new item value:", selvalue)
|
||||||
|
function input:on_save(value)
|
||||||
|
this:edit_item(selected, value)
|
||||||
|
end
|
||||||
|
function input:on_close()
|
||||||
|
InputDialog.on_close(self)
|
||||||
|
self:destroy()
|
||||||
|
this.dialog = false
|
||||||
|
end
|
||||||
|
input:show()
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
self.add = Button(self, "Add")
|
||||||
|
self.add:set_icon("B")
|
||||||
|
function self.add:on_click()
|
||||||
|
if not this.dialog then
|
||||||
|
---@type widget.inputdialog
|
||||||
|
local input = InputDialog("Add Item", "Enter the new item:")
|
||||||
|
function input:on_save(value)
|
||||||
|
this:add_item(value)
|
||||||
|
end
|
||||||
|
function input:on_close()
|
||||||
|
InputDialog.on_close(self)
|
||||||
|
self:destroy()
|
||||||
|
this.dialog = false
|
||||||
|
end
|
||||||
|
input:show()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self.remove = Button(self, "Remove")
|
||||||
|
self.remove:set_icon("C")
|
||||||
|
function self.remove:on_click()
|
||||||
|
local selected = this.list:get_selected()
|
||||||
|
if selected then
|
||||||
|
this:remove_item(selected)
|
||||||
|
else
|
||||||
|
MessageBox.error("No item selected", "Please select an item to remove")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Add a new item into the list.
|
||||||
|
---@param text widget.styledtext | string
|
||||||
|
---@param data any
|
||||||
|
function ItemsList:add_item(text, data)
|
||||||
|
if type(text) == "string" then
|
||||||
|
text = {text}
|
||||||
|
end
|
||||||
|
self.list:add_row(text, data)
|
||||||
|
self.list:set_visible_rows()
|
||||||
|
self:on_change()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Edit an existing item on the list.
|
||||||
|
---@param idx integer
|
||||||
|
---@param text widget.styledtext | string
|
||||||
|
---@param data any
|
||||||
|
function ItemsList:edit_item(idx, text, data)
|
||||||
|
if type(text) == "string" then
|
||||||
|
text = {text}
|
||||||
|
end
|
||||||
|
self.list:set_row(idx, text)
|
||||||
|
if data then
|
||||||
|
self.list:set_row_data(idx, data)
|
||||||
|
end
|
||||||
|
self:on_change()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Remove the given item from the list.
|
||||||
|
---@param idx integer
|
||||||
|
function ItemsList:remove_item(idx)
|
||||||
|
self.list:remove_row(idx)
|
||||||
|
self:on_change()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Return the items from the list.
|
||||||
|
---@return table<integer, string>
|
||||||
|
function ItemsList:get_items()
|
||||||
|
local output = {}
|
||||||
|
local count = #self.list.rows
|
||||||
|
for i=1, count, 1 do
|
||||||
|
table.insert(output, self.list:get_row_text(i))
|
||||||
|
end
|
||||||
|
return output
|
||||||
|
end
|
||||||
|
|
||||||
|
function ItemsList:update()
|
||||||
|
if not ItemsList.super.update(self) then return false end
|
||||||
|
|
||||||
|
if self.size.x == 0 then
|
||||||
|
self.size.x = self.add:get_width()
|
||||||
|
+ (style.padding.x / 2) + self.remove:get_width() + (50 * SCALE)
|
||||||
|
self.size.y = self.add:get_height() + (style.padding.y * 2) + 100
|
||||||
|
end
|
||||||
|
|
||||||
|
self.list:set_position(0, 0)
|
||||||
|
|
||||||
|
self.list:set_size(
|
||||||
|
self.size.x,
|
||||||
|
self.size.y - self.add:get_height() - (style.padding.y * 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.add:set_position(0, self.list:get_bottom() + style.padding.y)
|
||||||
|
|
||||||
|
self.remove:set_position(
|
||||||
|
self.add:get_right() + (style.padding.x / 2),
|
||||||
|
self.list:get_bottom() + style.padding.y
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return ItemsList
|
||||||
+257
@@ -0,0 +1,257 @@
|
|||||||
|
--
|
||||||
|
-- KeyBinding Dialog Widget.
|
||||||
|
-- @copyright Jefferson Gonzalez
|
||||||
|
-- @license MIT
|
||||||
|
--
|
||||||
|
|
||||||
|
local keymap = require "core.keymap"
|
||||||
|
local style = require "core.style"
|
||||||
|
local Button = require "libraries.widget.button"
|
||||||
|
local Dialog = require "libraries.widget.dialog"
|
||||||
|
local Label = require "libraries.widget.label"
|
||||||
|
local Line = require "libraries.widget.line"
|
||||||
|
local ListBox = require "libraries.widget.listbox"
|
||||||
|
local MessageBox = require "libraries.widget.messagebox"
|
||||||
|
|
||||||
|
---@type widget.keybinddialog
|
||||||
|
local current_dialog = nil
|
||||||
|
|
||||||
|
---@class widget.keybinddialog : widget.dialog
|
||||||
|
---@overload fun():widget.keybinddialog
|
||||||
|
---@field super widget.dialog
|
||||||
|
---@field selected integer
|
||||||
|
---@field shortcuts widget.listbox
|
||||||
|
---@field add widget.button
|
||||||
|
---@field remove widget.button
|
||||||
|
---@field line widget.line
|
||||||
|
---@field message widget.label
|
||||||
|
---@field binding widget.label
|
||||||
|
---@field mouse_intercept widget.label
|
||||||
|
---@field save widget.button
|
||||||
|
---@field reset widget.button
|
||||||
|
---@field cancel widget.button
|
||||||
|
local KeybindDialog = Dialog:extend()
|
||||||
|
|
||||||
|
---Constructor
|
||||||
|
function KeybindDialog:new()
|
||||||
|
KeybindDialog.super.new(self, "Keybinding Selector")
|
||||||
|
|
||||||
|
self.type_name = "widget.keybinddialog"
|
||||||
|
|
||||||
|
self.selected = nil
|
||||||
|
|
||||||
|
local this = self
|
||||||
|
|
||||||
|
self.shortcuts = ListBox(self.panel)
|
||||||
|
self.shortcuts:set_size(100, 100)
|
||||||
|
function self.shortcuts:on_row_click(idx, data)
|
||||||
|
this.selected = idx
|
||||||
|
end
|
||||||
|
|
||||||
|
self.add = Button(self.panel, "Add")
|
||||||
|
self.add:set_icon("B")
|
||||||
|
function self.add:on_click()
|
||||||
|
this.shortcuts:add_row({"none"})
|
||||||
|
this.shortcuts:set_selected(#this.shortcuts.rows)
|
||||||
|
this.shortcuts:set_visible_rows()
|
||||||
|
this.selected = #this.shortcuts.rows
|
||||||
|
end
|
||||||
|
|
||||||
|
self.remove = Button(self.panel, "Remove")
|
||||||
|
self.remove:set_icon("C")
|
||||||
|
function self.remove:on_click()
|
||||||
|
local selected = this.shortcuts:get_selected()
|
||||||
|
if selected then
|
||||||
|
this.shortcuts:remove_row(selected)
|
||||||
|
this.shortcuts:set_selected(nil)
|
||||||
|
this.selected = nil
|
||||||
|
else
|
||||||
|
MessageBox.error("No shortcut selected", "Please select an shortcut to remove")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self.line = Line(self.panel)
|
||||||
|
|
||||||
|
self.message = Label(self.panel, "Press a key combination to change selected")
|
||||||
|
|
||||||
|
self.mouse_intercept = Label(self.panel, "Grab mouse events here")
|
||||||
|
self.mouse_intercept.border.width = 1
|
||||||
|
self.mouse_intercept.clickable = true
|
||||||
|
self.mouse_intercept:set_size(100, 100)
|
||||||
|
function self.mouse_intercept:on_mouse_pressed(button, x, y, clicks)
|
||||||
|
keymap.on_mouse_pressed(button, x, y, clicks)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
function self.mouse_intercept:on_mouse_wheel(y)
|
||||||
|
keymap.on_mouse_wheel(y)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
function self.mouse_intercept:on_mouse_enter(...)
|
||||||
|
Label.super.on_mouse_enter(self, ...)
|
||||||
|
self.border.color = style.caret
|
||||||
|
end
|
||||||
|
function self.mouse_intercept:on_mouse_leave(...)
|
||||||
|
Label.super.on_mouse_leave(self, ...)
|
||||||
|
self.border.color = style.text
|
||||||
|
end
|
||||||
|
|
||||||
|
self.save = Button(self.panel, "Save")
|
||||||
|
self.save:set_icon("S")
|
||||||
|
function self.save:on_click()
|
||||||
|
this:on_save(this:get_bindings())
|
||||||
|
this:on_close()
|
||||||
|
end
|
||||||
|
|
||||||
|
self.reset = Button(self.panel, "Reset")
|
||||||
|
function self.reset:on_click()
|
||||||
|
this:on_reset()
|
||||||
|
this:on_close()
|
||||||
|
end
|
||||||
|
|
||||||
|
self.cancel = Button(self.panel, "Cancel")
|
||||||
|
self.cancel:set_icon("C")
|
||||||
|
function self.cancel:on_click()
|
||||||
|
this:on_close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param bindings table<integer, string>
|
||||||
|
function KeybindDialog:set_bindings(bindings)
|
||||||
|
self.shortcuts:clear()
|
||||||
|
for _, binding in ipairs(bindings) do
|
||||||
|
self.shortcuts:add_row({binding})
|
||||||
|
end
|
||||||
|
if #bindings > 0 then
|
||||||
|
self.shortcuts:set_selected(1)
|
||||||
|
self.selected = 1
|
||||||
|
end
|
||||||
|
self.shortcuts:set_visible_rows()
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return table<integer, string>
|
||||||
|
function KeybindDialog:get_bindings()
|
||||||
|
local bindings = {}
|
||||||
|
for idx=1, #self.shortcuts.rows, 1 do
|
||||||
|
table.insert(bindings, self.shortcuts:get_row_text(idx))
|
||||||
|
end
|
||||||
|
return bindings
|
||||||
|
end
|
||||||
|
|
||||||
|
---Show the dialog and enable key interceptions
|
||||||
|
function KeybindDialog:show()
|
||||||
|
current_dialog = self
|
||||||
|
KeybindDialog.super.show(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Hide the dialog and disable key interceptions
|
||||||
|
function KeybindDialog:hide()
|
||||||
|
current_dialog = nil
|
||||||
|
KeybindDialog.super.hide(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Called when the user clicks on save
|
||||||
|
---@param bindings string
|
||||||
|
function KeybindDialog:on_save(bindings) end
|
||||||
|
|
||||||
|
---Called when the user clicks on reset
|
||||||
|
function KeybindDialog:on_reset() end
|
||||||
|
|
||||||
|
function KeybindDialog:update()
|
||||||
|
if not KeybindDialog.super.update(self) then return false end
|
||||||
|
|
||||||
|
self.shortcuts:set_position(style.padding.x/2, 0)
|
||||||
|
|
||||||
|
self.add:set_position(
|
||||||
|
style.padding.x/2,
|
||||||
|
self.shortcuts:get_bottom() + style.padding.y
|
||||||
|
)
|
||||||
|
|
||||||
|
self.remove:set_position(
|
||||||
|
self.add:get_right() + (style.padding.x/2),
|
||||||
|
self.shortcuts:get_bottom() + style.padding.y
|
||||||
|
)
|
||||||
|
|
||||||
|
self.line:set_position(
|
||||||
|
0,
|
||||||
|
self.remove:get_bottom() + style.padding.y
|
||||||
|
)
|
||||||
|
|
||||||
|
self.message:set_position(
|
||||||
|
style.padding.x/2,
|
||||||
|
self.line:get_bottom() + style.padding.y
|
||||||
|
)
|
||||||
|
self.mouse_intercept:set_position(
|
||||||
|
style.padding.x/2,
|
||||||
|
self.message:get_bottom() + style.padding.y
|
||||||
|
)
|
||||||
|
|
||||||
|
self.save:set_position(
|
||||||
|
style.padding.x/2,
|
||||||
|
self.mouse_intercept:get_bottom() + style.padding.y
|
||||||
|
)
|
||||||
|
self.reset:set_position(
|
||||||
|
self.save:get_right() + style.padding.x,
|
||||||
|
self.mouse_intercept:get_bottom() + style.padding.y
|
||||||
|
)
|
||||||
|
self.cancel:set_position(
|
||||||
|
self.reset:get_right() + style.padding.x,
|
||||||
|
self.mouse_intercept:get_bottom() + style.padding.y
|
||||||
|
)
|
||||||
|
|
||||||
|
self.panel.size.x = self.panel:get_real_width() + style.padding.x
|
||||||
|
self.panel.size.y = self.panel:get_real_height()
|
||||||
|
self.size.x = self:get_real_width() - (style.padding.x / 2)
|
||||||
|
self.size.y = self:get_real_height() + (style.padding.y / 2)
|
||||||
|
|
||||||
|
self.shortcuts:set_size(self.size.x - style.padding.x)
|
||||||
|
|
||||||
|
self.line:set_width(self.size.x - style.padding.x)
|
||||||
|
|
||||||
|
self.mouse_intercept:set_size(self.size.x - style.padding.x)
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Intercept keymap events
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Same declarations as in core.keymap because modkey_map is not public
|
||||||
|
local macos = PLATFORM == "Mac OS X"
|
||||||
|
local modkeys_os = require("core.modkeys-" .. (macos and "macos" or "generic"))
|
||||||
|
local modkey_map = modkeys_os.map
|
||||||
|
local modkeys = modkeys_os.keys
|
||||||
|
|
||||||
|
---Copied from core.keymap because it is not public
|
||||||
|
local function key_to_stroke(k)
|
||||||
|
local stroke = ""
|
||||||
|
for _, mk in ipairs(modkeys) do
|
||||||
|
if keymap.modkeys[mk] then
|
||||||
|
stroke = stroke .. mk .. "+"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return stroke .. k
|
||||||
|
end
|
||||||
|
|
||||||
|
local keymap_on_key_pressed = keymap.on_key_pressed
|
||||||
|
function keymap.on_key_pressed(k, ...)
|
||||||
|
if current_dialog and current_dialog.selected then
|
||||||
|
local mk = modkey_map[k]
|
||||||
|
if mk then
|
||||||
|
keymap.modkeys[mk] = true
|
||||||
|
-- work-around for windows where `altgr` is treated as `ctrl+alt`
|
||||||
|
if mk == "altgr" then
|
||||||
|
keymap.modkeys["ctrl"] = false
|
||||||
|
end
|
||||||
|
current_dialog.shortcuts:set_row(current_dialog.selected, {key_to_stroke("")})
|
||||||
|
else
|
||||||
|
current_dialog.shortcuts:set_row(current_dialog.selected, {key_to_stroke(k)})
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return keymap_on_key_pressed(k, ...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return KeybindDialog
|
||||||
+95
@@ -0,0 +1,95 @@
|
|||||||
|
--
|
||||||
|
-- Label Widget.
|
||||||
|
-- @copyright Jefferson Gonzalez
|
||||||
|
-- @license MIT
|
||||||
|
--
|
||||||
|
|
||||||
|
local style = require "core.style"
|
||||||
|
local Widget = require "libraries.widget"
|
||||||
|
|
||||||
|
---@class widget.label : widget
|
||||||
|
---@overload fun(parent:widget?, label?:string):widget.label
|
||||||
|
---@field clickable boolean
|
||||||
|
local Label = Widget:extend()
|
||||||
|
|
||||||
|
---Constructor
|
||||||
|
---@param parent widget
|
||||||
|
---@param label string
|
||||||
|
function Label:new(parent, label)
|
||||||
|
Label.super.new(self, parent)
|
||||||
|
self.type_name = "widget.label"
|
||||||
|
self.clickable = false
|
||||||
|
self.border.width = 0
|
||||||
|
self.custom_size = {x = 0, y = 0}
|
||||||
|
|
||||||
|
self:set_label(label or "")
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param width? integer
|
||||||
|
---@param height? integer
|
||||||
|
function Label:set_size(width, height)
|
||||||
|
Label.super.set_size(self, width, height)
|
||||||
|
self.custom_size.x = self.size.x
|
||||||
|
self.custom_size.y = self.size.y
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set the label text and recalculates the widget size.
|
||||||
|
---@param text string|widget.styledtext
|
||||||
|
function Label:set_label(text)
|
||||||
|
Label.super.set_label(self, text)
|
||||||
|
|
||||||
|
local font = self:get_font()
|
||||||
|
|
||||||
|
if self.custom_size.x <= 0 then
|
||||||
|
if type(text) == "table" then
|
||||||
|
self.size.x, self.size.y = self:draw_styled_text(text, 0, 0, true)
|
||||||
|
else
|
||||||
|
self.size.x = font:get_width(self.label)
|
||||||
|
self.size.y = font:get_height()
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.border.width > 0 then
|
||||||
|
self.size.x = self.size.x + style.padding.x
|
||||||
|
self.size.y = self.size.y + style.padding.y
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Label:update()
|
||||||
|
if not Label.super.update(self) then return false end
|
||||||
|
|
||||||
|
if self.custom_size.x <= 0 then
|
||||||
|
-- update the size
|
||||||
|
self:set_label(self.label)
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function Label:draw()
|
||||||
|
if not self:is_visible() then return false end
|
||||||
|
|
||||||
|
self:draw_border()
|
||||||
|
|
||||||
|
local px = self.border.width > 0 and (style.padding.x / 2) or 0
|
||||||
|
local py = self.border.width > 0 and (style.padding.y / 2) or 0
|
||||||
|
|
||||||
|
local posx, posy = self.position.x + px, self.position.y + py
|
||||||
|
|
||||||
|
if type(self.label) == "table" then
|
||||||
|
self:draw_styled_text(self.label, posx, posy)
|
||||||
|
else
|
||||||
|
renderer.draw_text(
|
||||||
|
self:get_font(),
|
||||||
|
self.label,
|
||||||
|
posx,
|
||||||
|
posy,
|
||||||
|
self.foreground_color or style.text
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return Label
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
--
|
||||||
|
-- Line Widget.
|
||||||
|
-- @copyright Jefferson Gonzalez
|
||||||
|
-- @license MIT
|
||||||
|
--
|
||||||
|
|
||||||
|
local style = require "core.style"
|
||||||
|
local Widget = require "libraries.widget"
|
||||||
|
|
||||||
|
---@class widget.line : widget
|
||||||
|
---@overload fun(parent?:widget, thickness?:integer, padding?:number):widget.line
|
||||||
|
---@field public padding integer
|
||||||
|
---@field private custom_width number
|
||||||
|
local Line = Widget:extend()
|
||||||
|
|
||||||
|
---Constructor
|
||||||
|
---@param parent widget
|
||||||
|
---@param thickness integer
|
||||||
|
---@param padding number
|
||||||
|
function Line:new(parent, thickness, padding)
|
||||||
|
Line.super.new(self, parent)
|
||||||
|
self.type_name = "widget.line"
|
||||||
|
self.size.y = thickness or 2
|
||||||
|
self.custom_width = nil
|
||||||
|
self.border.width = 0
|
||||||
|
self.padding = padding or (style.padding.x / 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set the thickness of the line
|
||||||
|
---@param thickness number
|
||||||
|
function Line:set_thickness(thickness)
|
||||||
|
self.size.y = thickness or 2
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set a custom width for the line
|
||||||
|
---@param width number
|
||||||
|
function Line:set_width(width)
|
||||||
|
self.custom_width = width
|
||||||
|
self.size.x = width
|
||||||
|
end
|
||||||
|
|
||||||
|
function Line:draw()
|
||||||
|
if not self:is_visible() then return false end
|
||||||
|
|
||||||
|
if not self.custom_width then
|
||||||
|
self.size.x = self.parent.size.x - (self.padding * 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
renderer.draw_rect(
|
||||||
|
self.position.x + self.padding,
|
||||||
|
self.position.y,
|
||||||
|
self.size.x,
|
||||||
|
self.size.y,
|
||||||
|
self.foreground_color or style.caret
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return Line
|
||||||
+926
@@ -0,0 +1,926 @@
|
|||||||
|
--
|
||||||
|
-- ListBox Widget.
|
||||||
|
-- @copyright Jefferson Gonzalez
|
||||||
|
-- @license MIT
|
||||||
|
--
|
||||||
|
|
||||||
|
local core = require "core"
|
||||||
|
local style = require "core.style"
|
||||||
|
local Widget = require "libraries.widget"
|
||||||
|
local MessageBox = require "libraries.widget.messagebox"
|
||||||
|
|
||||||
|
---@class widget.listbox.column
|
||||||
|
---@field public name string
|
||||||
|
---@field public width string
|
||||||
|
---@field public expand boolean
|
||||||
|
---@field public longest integer
|
||||||
|
|
||||||
|
---@alias widget.listbox.drawcol fun(self, row, x, y, font, color, only_calc)
|
||||||
|
---@alias widget.listbox.filtercb fun(self:widget.listbox, idx:integer, row:widget.listbox.row, data:any):number?
|
||||||
|
|
||||||
|
---@alias widget.listbox.row table<integer, renderer.font|widget.fontreference|renderer.color|integer|string|widget.listbox.drawcol>
|
||||||
|
|
||||||
|
---@alias widget.listbox.colpos table<integer,integer>
|
||||||
|
|
||||||
|
---@class widget.listbox : widget
|
||||||
|
---@overload fun(parent:widget?):widget.listbox
|
||||||
|
---@field rows widget.listbox.row[]
|
||||||
|
---@field private row_data any
|
||||||
|
---@field private rows_original widget.listbox.row[]
|
||||||
|
---@field private row_data_original any
|
||||||
|
---@field private columns widget.listbox.column[]
|
||||||
|
---@field private positions widget.listbox.colpos[]
|
||||||
|
---@field private mouse widget.position
|
||||||
|
---@field private selected_row integer
|
||||||
|
---@field private hovered_row integer
|
||||||
|
---@field private largest_row integer
|
||||||
|
---@field private expand boolean
|
||||||
|
---@field private visible_rows table<integer, integer>
|
||||||
|
---@field private visible_rendered boolean
|
||||||
|
---@field private last_scale integer
|
||||||
|
---@field private last_offset integer
|
||||||
|
local ListBox = Widget:extend()
|
||||||
|
|
||||||
|
---Indicates on a widget.listbox.row that the end
|
||||||
|
---of a column was reached.
|
||||||
|
---@type integer
|
||||||
|
ListBox.COLEND = 1
|
||||||
|
|
||||||
|
---Indicates on a widget.listbox.row that a new line
|
||||||
|
---follows while still rendering the same column.
|
||||||
|
---@type integer
|
||||||
|
ListBox.NEWLINE = 2
|
||||||
|
|
||||||
|
---Constructor
|
||||||
|
---@param parent widget
|
||||||
|
function ListBox:new(parent)
|
||||||
|
ListBox.super.new(self, parent)
|
||||||
|
self.type_name = "widget.listbox"
|
||||||
|
self.scrollable = true
|
||||||
|
self.rows = {}
|
||||||
|
self.row_data = {}
|
||||||
|
self.rows_original = {}
|
||||||
|
self.row_data_original = {}
|
||||||
|
self.rows_idx_original = {}
|
||||||
|
self.columns = {}
|
||||||
|
self.positions = {}
|
||||||
|
self.selected_row = 0
|
||||||
|
self.hovered_row = 0
|
||||||
|
self.largest_row = 0
|
||||||
|
self.expand = false
|
||||||
|
self.visible_rows = {}
|
||||||
|
self.visible_rendered = false
|
||||||
|
self.last_scale = 0
|
||||||
|
self.last_offset = 0
|
||||||
|
|
||||||
|
self:set_size(200, (self:get_font():get_height() + (style.padding.y*2)) * 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set which rows to show using the specified match string or callback,
|
||||||
|
---if nil all rows are restored.
|
||||||
|
---@param match? string | widget.listbox.filtercb
|
||||||
|
function ListBox:filter(match)
|
||||||
|
if (not match or match == "") and #self.rows_original > 0 then
|
||||||
|
self:clear()
|
||||||
|
for idx, row in ipairs(self.rows_original) do
|
||||||
|
self:add_row(row, self.row_data_original[idx])
|
||||||
|
end
|
||||||
|
self.rows_original = {}
|
||||||
|
self.row_data_original = {}
|
||||||
|
self.rows_idx_original = {}
|
||||||
|
return
|
||||||
|
elseif match and match ~= "" then
|
||||||
|
self.rows_original = #self.rows_original > 0
|
||||||
|
and self.rows_original or self.rows
|
||||||
|
self.row_data_original = #self.row_data_original > 0
|
||||||
|
and self.row_data_original or self.row_data
|
||||||
|
self.rows_idx_original = {}
|
||||||
|
|
||||||
|
self:clear()
|
||||||
|
|
||||||
|
local match_type = type(match)
|
||||||
|
|
||||||
|
local rows = {}
|
||||||
|
for idx, row in ipairs(self.rows_original) do
|
||||||
|
local score
|
||||||
|
if match_type == "function" then
|
||||||
|
score = match(self, idx, row, self.row_data_original[idx])
|
||||||
|
else
|
||||||
|
score = system.fuzzy_match(self:get_row_text(row), match, false)
|
||||||
|
end
|
||||||
|
if score then
|
||||||
|
table.insert(rows, {row, self.row_data_original[idx], score, idx})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.sort(rows, function(a, b) return a[3] > b[3] end)
|
||||||
|
|
||||||
|
for _, row in ipairs(rows) do
|
||||||
|
self:add_row(row[1], row[2])
|
||||||
|
table.insert(self.rows_idx_original, row[4])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---If no width is given column will be set to automatically
|
||||||
|
---expand depending on the longest element
|
||||||
|
---@param name string
|
||||||
|
---@param width? number
|
||||||
|
---@param expand? boolean
|
||||||
|
function ListBox:add_column(name, width, expand)
|
||||||
|
local column = {
|
||||||
|
name = name,
|
||||||
|
width = width or self:get_font():get_width(name),
|
||||||
|
expand = expand and expand or (width and false or true)
|
||||||
|
}
|
||||||
|
|
||||||
|
table.insert(self.columns, column)
|
||||||
|
end
|
||||||
|
|
||||||
|
---You can give it a table a la statusview style where you pass elements
|
||||||
|
---like fonts, colors, ListBox.COLEND, ListBox.NEWLINE and multiline strings.
|
||||||
|
---@param row widget.listbox.row
|
||||||
|
---@param data any Associated with the row and given to on_row_click()
|
||||||
|
function ListBox:add_row(row, data)
|
||||||
|
table.insert(self.rows, row)
|
||||||
|
table.insert(self.positions, self:get_col_positions(row))
|
||||||
|
|
||||||
|
if type(data) ~= "nil" then
|
||||||
|
self.row_data[#self.rows] = data
|
||||||
|
end
|
||||||
|
|
||||||
|
-- increase columns width if needed
|
||||||
|
if #self.columns > 0 then
|
||||||
|
local ridx = #self.rows
|
||||||
|
for col, pos in ipairs(self.positions[ridx]) do
|
||||||
|
if self.columns[col].expand then
|
||||||
|
local w = self:draw_row_range(ridx, row, pos[1], pos[2], 1, 1, true)
|
||||||
|
|
||||||
|
-- store the row with longest column for cheaper calculation
|
||||||
|
self.columns[col].width = math.max(self.columns[col].width, w)
|
||||||
|
if self.columns[col].width < w then
|
||||||
|
self.columns[col].longest = ridx
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- precalculate the row size and position
|
||||||
|
self:calc_row_size_pos(#self.rows)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Calculate a row position and size and store it on the row it
|
||||||
|
---self on the fields x, y, w, h
|
||||||
|
---@param ridx integer
|
||||||
|
function ListBox:calc_row_size_pos(ridx)
|
||||||
|
local x = self.border.width
|
||||||
|
local y = self.border.width
|
||||||
|
|
||||||
|
if ridx == 1 then
|
||||||
|
-- if columns are enabled leave some space to render them
|
||||||
|
if #self.columns > 0 then
|
||||||
|
y = y + self:get_font():get_height() + style.padding.y
|
||||||
|
end
|
||||||
|
else
|
||||||
|
y = y + self.rows[ridx-1].y + self.rows[ridx-1].h
|
||||||
|
end
|
||||||
|
|
||||||
|
self:draw_row(ridx, x, y, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Recalculate all row sizes and positions which should be only required
|
||||||
|
---when lite-xl ui scale changes or a row is removed from the list
|
||||||
|
function ListBox:recalc_all_rows()
|
||||||
|
for ridx, _ in ipairs(self.rows) do
|
||||||
|
self:calc_row_size_pos(ridx)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Calculates the scrollable size based on the last row of the list.
|
||||||
|
---@return number
|
||||||
|
function ListBox:get_scrollable_size()
|
||||||
|
local size = self.size.y
|
||||||
|
local rows = #self.rows
|
||||||
|
if rows > 0 and self.rows[rows].y then
|
||||||
|
size = math.max(size, self.rows[rows].y + self.rows[rows].h)
|
||||||
|
end
|
||||||
|
return size
|
||||||
|
end
|
||||||
|
|
||||||
|
function ListBox:get_h_scrollable_size()
|
||||||
|
local size = self.size.x
|
||||||
|
local rows = #self.rows
|
||||||
|
if rows > 0 and self.rows[rows].x then
|
||||||
|
size = math.max(size, self.rows[rows].x + self.rows[rows].w)
|
||||||
|
end
|
||||||
|
return size
|
||||||
|
end
|
||||||
|
|
||||||
|
---Detects the rows that are visible each time the content is scrolled,
|
||||||
|
---this way the draw function will only process the visible rows.
|
||||||
|
function ListBox:set_visible_rows()
|
||||||
|
local _, oy = self:get_content_offset()
|
||||||
|
local h = self.size.y
|
||||||
|
|
||||||
|
-- substract column heading from list height
|
||||||
|
local colh = 0
|
||||||
|
if #self.columns > 0 then
|
||||||
|
colh = self:get_font():get_height() + style.padding.y
|
||||||
|
h = h - colh
|
||||||
|
end
|
||||||
|
|
||||||
|
-- start from nearest row relative to scroll direction for
|
||||||
|
-- better performance on long lists
|
||||||
|
local idx, total, step = 1, #self.rows, 1
|
||||||
|
if #self.visible_rows > 0 then
|
||||||
|
if oy < self.last_offset or not self.visible_rendered then
|
||||||
|
idx = self.visible_rows[1]
|
||||||
|
self.visible_rendered = true
|
||||||
|
else
|
||||||
|
idx = self.visible_rows[#self.visible_rows]
|
||||||
|
total = 1
|
||||||
|
step = -1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
oy = oy - self.position.y
|
||||||
|
|
||||||
|
self.visible_rows = {}
|
||||||
|
local first_visible = false
|
||||||
|
local height = 0
|
||||||
|
for i=idx, total, step do
|
||||||
|
local row = self.rows[i]
|
||||||
|
if row then
|
||||||
|
local top = row.y - colh + row.h + oy
|
||||||
|
local visible = false
|
||||||
|
local visible_area = h - top
|
||||||
|
if top < 0 and (top + row.h) > 0 then
|
||||||
|
visible = true
|
||||||
|
elseif top >= 0 and top < h then
|
||||||
|
visible = true
|
||||||
|
end
|
||||||
|
if visible and height <= h then
|
||||||
|
table.insert(self.visible_rows, i)
|
||||||
|
first_visible = true
|
||||||
|
-- store only the visible height
|
||||||
|
if top < 0 then
|
||||||
|
height = height + (top + row.h)
|
||||||
|
else
|
||||||
|
if visible_area > row.h then
|
||||||
|
height = height + row.h
|
||||||
|
else
|
||||||
|
height = height + visible_area
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif first_visible then
|
||||||
|
table.insert(self.visible_rows, i)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- append one more row if possible to fill possible empty spaces of
|
||||||
|
-- incomplete row height calculation above (bad math skills workarounds)
|
||||||
|
local last_row = self.visible_rows[#self.visible_rows]
|
||||||
|
local first_row = self.visible_rows[1]
|
||||||
|
if #self.visible_rows > 0 then
|
||||||
|
if step == 1 then
|
||||||
|
if self.rows[last_row+1] then
|
||||||
|
table.insert(self.visible_rows, last_row+1)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if self.rows[first_row-2] and first_row-2 ~= 1 then
|
||||||
|
table.insert(self.visible_rows, first_row-2)
|
||||||
|
elseif self.rows[last_row+1] then
|
||||||
|
table.insert(self.visible_rows, last_row+1)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- sort for proper subsequent loop interations
|
||||||
|
table.sort(
|
||||||
|
self.visible_rows,
|
||||||
|
function(val1, val2) return val1 < val2 end
|
||||||
|
)
|
||||||
|
|
||||||
|
local frow = self.visible_rows[1]
|
||||||
|
for i, _ in ipairs(self.visible_rows) do
|
||||||
|
if self.rows[frow] then
|
||||||
|
self.visible_rows[i] = frow
|
||||||
|
frow = frow + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #self.visible_rows > 1 then
|
||||||
|
if
|
||||||
|
self.visible_rows[#self.visible_rows]
|
||||||
|
==
|
||||||
|
self.visible_rows[#self.visible_rows-1]
|
||||||
|
then
|
||||||
|
table.remove(self.visible_rows, #self.visible_rows)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Solution to safely remove elements from array table:
|
||||||
|
-- found at https://stackoverflow.com/a/53038524
|
||||||
|
local function array_remove(t, fnKeep)
|
||||||
|
local j, n = 1, #t;
|
||||||
|
|
||||||
|
for i=1, n do
|
||||||
|
if (fnKeep(t, i, j)) then
|
||||||
|
if (i ~= j) then
|
||||||
|
t[j] = t[i];
|
||||||
|
t[i] = nil;
|
||||||
|
end
|
||||||
|
j = j + 1;
|
||||||
|
else
|
||||||
|
t[i] = nil;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return t;
|
||||||
|
end
|
||||||
|
|
||||||
|
---Remove a given row index from the list.
|
||||||
|
---@param ridx integer
|
||||||
|
function ListBox:remove_row(ridx)
|
||||||
|
if not self.rows[ridx] then return end
|
||||||
|
|
||||||
|
if #self.rows_idx_original > 0 then
|
||||||
|
MessageBox.error(
|
||||||
|
"Can not remove row",
|
||||||
|
"Rows can not be removed when the list is filtered."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local last_col = false
|
||||||
|
local row_y = self.rows[ridx].y
|
||||||
|
local row_h = self.rows[ridx].h
|
||||||
|
if ridx == #self.rows then
|
||||||
|
last_col = true
|
||||||
|
end
|
||||||
|
|
||||||
|
local fields = { "rows", "positions", "row_data" }
|
||||||
|
for _, field in ipairs(fields) do
|
||||||
|
array_remove(self[field], function(_, i, _)
|
||||||
|
if i == ridx then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
for _, col in ipairs(self.columns) do
|
||||||
|
if col.longest == ridx then
|
||||||
|
col.longest = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not last_col and #self.rows > 0 then
|
||||||
|
for idx=ridx, #self.rows, 1 do
|
||||||
|
self.rows[idx].y = self.rows[idx].y - row_h
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local visible_removed = false
|
||||||
|
array_remove(self.visible_rows, function(t, i, _)
|
||||||
|
if t[i] == ridx then
|
||||||
|
visible_removed = true
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- make visible rows sequence correctly incremental
|
||||||
|
if visible_removed and #self.visible_rows > 0 then
|
||||||
|
local first_row = self.visible_rows[1]
|
||||||
|
for i, _ in ipairs(self.visible_rows) do
|
||||||
|
self.visible_rows[i] = first_row
|
||||||
|
first_row = first_row + 1
|
||||||
|
end
|
||||||
|
self:set_visible_rows()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set the row that is currently active/selected.
|
||||||
|
---@param idx? integer
|
||||||
|
function ListBox:set_selected(idx)
|
||||||
|
self.selected_row = idx or 0
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the row that is currently active/selected.
|
||||||
|
---@return integer | nil
|
||||||
|
function ListBox:get_selected()
|
||||||
|
if self.selected_row > 0 then
|
||||||
|
return self.selected_row
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---Change the content assigned to a row.
|
||||||
|
---@param idx integer
|
||||||
|
---@param row widget.listbox.row
|
||||||
|
function ListBox:set_row(idx, row)
|
||||||
|
--TODO: recalculate subsequent row sizes and max col width if needed
|
||||||
|
if self.rows[idx] then
|
||||||
|
self.rows[idx] = row
|
||||||
|
if #self.rows_idx_original > 0 then
|
||||||
|
self.rows_original[self.rows_idx_original[idx]] = row
|
||||||
|
end
|
||||||
|
-- precalculate the row size and position
|
||||||
|
self:calc_row_size_pos(idx)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Change the data assigned to a row.
|
||||||
|
---@param idx integer
|
||||||
|
---@param data any|nil
|
||||||
|
function ListBox:set_row_data(idx, data)
|
||||||
|
if self.rows[idx] then
|
||||||
|
self.row_data[idx] = data
|
||||||
|
if #self.rows_idx_original > 0 then
|
||||||
|
self.row_data_original[self.rows_idx_original[idx]] = data
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the data associated with a row.
|
||||||
|
---@param idx integer
|
||||||
|
---@return any|nil
|
||||||
|
function ListBox:get_row_data(idx)
|
||||||
|
if type(self.row_data[idx]) ~= "nil" then
|
||||||
|
return self.row_data[idx]
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the text only of a styled row.
|
||||||
|
---@param row integer | table
|
||||||
|
---@return string
|
||||||
|
function ListBox:get_row_text(row)
|
||||||
|
local text = ""
|
||||||
|
row = type(row) == "table" and row or self.rows[row]
|
||||||
|
if row then
|
||||||
|
for _, element in ipairs(row) do
|
||||||
|
if type(element) == "string" then
|
||||||
|
text = text .. element
|
||||||
|
elseif element == ListBox.NEWLINE then
|
||||||
|
text = text .. "\n"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return text
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the starting and ending position of columns in a row table.
|
||||||
|
---@param row widget.listbox.row
|
||||||
|
---@return widget.listbox.colpos
|
||||||
|
function ListBox:get_col_positions(row)
|
||||||
|
local positions = {}
|
||||||
|
local idx = 1
|
||||||
|
local idx_start = 1
|
||||||
|
local row_len = #row
|
||||||
|
|
||||||
|
for _, element in ipairs(row) do
|
||||||
|
if element == ListBox.COLEND then
|
||||||
|
table.insert(positions, { idx_start, idx-1 })
|
||||||
|
idx_start = idx + 1
|
||||||
|
elseif idx == row_len then
|
||||||
|
table.insert(positions, { idx_start, idx })
|
||||||
|
end
|
||||||
|
idx = idx + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
return positions
|
||||||
|
end
|
||||||
|
|
||||||
|
---Move a row to the desired position if possible.
|
||||||
|
---@param idx integer
|
||||||
|
---@param pos integer
|
||||||
|
---@return boolean moved
|
||||||
|
function ListBox:move_row_to(idx, pos)
|
||||||
|
if idx == pos or (pos == #self.rows and #self.rows == 1) then return false end
|
||||||
|
|
||||||
|
if pos < 1 then pos = 1 end
|
||||||
|
|
||||||
|
local row = table.remove(self.rows, idx)
|
||||||
|
local position = table.remove(self.positions, idx)
|
||||||
|
|
||||||
|
if pos <= #self.rows then
|
||||||
|
table.insert(self.rows, pos, row)
|
||||||
|
table.insert(self.positions, pos, position)
|
||||||
|
else
|
||||||
|
table.insert(self.rows, row)
|
||||||
|
table.insert(self.positions, position)
|
||||||
|
pos = #self.rows
|
||||||
|
end
|
||||||
|
|
||||||
|
local moved_row_data = self.row_data[idx]
|
||||||
|
local swapped_row_data = self.row_data[pos]
|
||||||
|
self.row_data[idx] = swapped_row_data
|
||||||
|
self.row_data[pos] = moved_row_data
|
||||||
|
|
||||||
|
self.selected_row = pos
|
||||||
|
|
||||||
|
self:recalc_all_rows()
|
||||||
|
self:set_visible_rows()
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
---Move a row one position up if possible.
|
||||||
|
---@param idx integer
|
||||||
|
---@return boolean moved
|
||||||
|
function ListBox:move_row_up(idx)
|
||||||
|
return self:move_row_to(idx, idx-1)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Move a row one position down if possible.
|
||||||
|
---@param idx integer
|
||||||
|
---@return boolean moved
|
||||||
|
function ListBox:move_row_down(idx)
|
||||||
|
self:move_row_to(idx, idx+1)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Enables expanding the element to total size of parent on content updates.
|
||||||
|
function ListBox:enable_expand(expand)
|
||||||
|
self.expand = expand
|
||||||
|
if expand then
|
||||||
|
self:resize_to_parent()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Resizes the listbox to match the parent size
|
||||||
|
function ListBox:resize_to_parent()
|
||||||
|
self.size.x = self.parent.size.x
|
||||||
|
- (self.border.width * 2)
|
||||||
|
|
||||||
|
self.size.y = self.parent.size.y
|
||||||
|
- (self.border.width * 2)
|
||||||
|
|
||||||
|
self:set_visible_rows()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Remove all the rows from the listbox.
|
||||||
|
function ListBox:clear()
|
||||||
|
self.rows = {}
|
||||||
|
self.row_data = {}
|
||||||
|
self.positions = {}
|
||||||
|
self.selected_row = 0
|
||||||
|
self.hovered_row = 0
|
||||||
|
|
||||||
|
for cidx, col in ipairs(self.columns) do
|
||||||
|
col.width = self:get_col_width(cidx)
|
||||||
|
col.longest = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
self:set_visible_rows()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Render or calculate the size of the specified range of elements in a row.
|
||||||
|
---@param ridx integer
|
||||||
|
---@param row widget.listbox.row
|
||||||
|
---@param start_idx integer
|
||||||
|
---@param end_idx integer
|
||||||
|
---@param x integer
|
||||||
|
---@param y integer
|
||||||
|
---@param only_calc boolean
|
||||||
|
---@return integer width
|
||||||
|
---@return integer height
|
||||||
|
function ListBox:draw_row_range(ridx, row, start_idx, end_idx, x, y, only_calc)
|
||||||
|
local font = self:get_font()
|
||||||
|
local color = self.foreground_color or style.text
|
||||||
|
local width = 0
|
||||||
|
local height = font:get_height()
|
||||||
|
local new_line = false
|
||||||
|
local nx = x
|
||||||
|
|
||||||
|
for pos=start_idx, end_idx, 1 do
|
||||||
|
local element = row[pos]
|
||||||
|
local ele_type = type(element)
|
||||||
|
if
|
||||||
|
ele_type == "userdata"
|
||||||
|
or
|
||||||
|
(
|
||||||
|
ele_type == "table"
|
||||||
|
and
|
||||||
|
(element.container or type(element[1]) == "userdata")
|
||||||
|
)
|
||||||
|
then
|
||||||
|
if ele_type == "table" and element.container then
|
||||||
|
font = element.container[element.name]
|
||||||
|
else
|
||||||
|
font = element
|
||||||
|
end
|
||||||
|
elseif ele_type == "table" then
|
||||||
|
color = element
|
||||||
|
elseif element == ListBox.NEWLINE then
|
||||||
|
y = y + font:get_height()
|
||||||
|
nx = x
|
||||||
|
new_line = true
|
||||||
|
elseif ele_type == "function" then
|
||||||
|
local w, h = element(self, ridx, nx, y, font, color, only_calc)
|
||||||
|
nx = nx + width
|
||||||
|
height = math.max(height, h)
|
||||||
|
width = width + w
|
||||||
|
elseif ele_type == "string" then
|
||||||
|
local rx, ry, w, h = self:draw_text_multiline(
|
||||||
|
font, element, nx, y, color, only_calc
|
||||||
|
)
|
||||||
|
y = ry
|
||||||
|
nx = rx
|
||||||
|
if new_line then
|
||||||
|
height = height + h
|
||||||
|
width = math.max(width, w)
|
||||||
|
new_line = false
|
||||||
|
else
|
||||||
|
height = math.max(height, h)
|
||||||
|
width = width + w
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return width, height
|
||||||
|
end
|
||||||
|
|
||||||
|
---Calculate the overall width of a column.
|
||||||
|
---@param col integer
|
||||||
|
---@return number
|
||||||
|
function ListBox:get_col_width(col)
|
||||||
|
if self.columns[col] then
|
||||||
|
if not self.columns[col].expand then
|
||||||
|
return self.columns[col].width
|
||||||
|
else
|
||||||
|
-- if longest is available don't iterate the entire row list
|
||||||
|
if self.columns[col].longest then
|
||||||
|
local id = self.columns[col].longest
|
||||||
|
local width = self:draw_row_range(
|
||||||
|
id,
|
||||||
|
self.rows[id],
|
||||||
|
self.positions[id][col][1],
|
||||||
|
self.positions[id][col][2],
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
return width
|
||||||
|
end
|
||||||
|
|
||||||
|
local width = self:get_font():get_width(self.columns[col].name)
|
||||||
|
for id, row in ipairs(self.rows) do
|
||||||
|
local w, h = self:draw_row_range(
|
||||||
|
id,
|
||||||
|
row,
|
||||||
|
self.positions[id][col][1],
|
||||||
|
self.positions[id][col][2],
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
width = math.max(width, w)
|
||||||
|
end
|
||||||
|
return width
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
---Draw the column headers of the list if available
|
||||||
|
---@param w integer
|
||||||
|
---@param h integer
|
||||||
|
---@param ox number Horizontal offset
|
||||||
|
function ListBox:draw_header(w, h, ox)
|
||||||
|
local x = self.position.x
|
||||||
|
local y = self.position.y
|
||||||
|
renderer.draw_rect(x, y, w, h, style.background2)
|
||||||
|
x = ox + self.position.x
|
||||||
|
for _, col in ipairs(self.columns) do
|
||||||
|
renderer.draw_text(
|
||||||
|
self:get_font(),
|
||||||
|
col.name,
|
||||||
|
x + style.padding.x / 2,
|
||||||
|
y + style.padding.y / 2,
|
||||||
|
style.accent
|
||||||
|
)
|
||||||
|
x = x + col.width + style.padding.x
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Draw or calculate the dimensions of the given row position and stores
|
||||||
|
---the size and position on the row it self.
|
||||||
|
---@param row integer
|
||||||
|
---@param x integer
|
||||||
|
---@param y integer
|
||||||
|
---@param only_calc? boolean
|
||||||
|
---@return integer width
|
||||||
|
---@return integer height
|
||||||
|
function ListBox:draw_row(row, x, y, only_calc)
|
||||||
|
local w, h = 0, 0
|
||||||
|
|
||||||
|
if not only_calc and self.rows[row].w then
|
||||||
|
w, h = self.rows[row].w, self.rows[row].h
|
||||||
|
w = self.largest_row > 0 and self.largest_row or w
|
||||||
|
|
||||||
|
if self.selected_row == row then
|
||||||
|
renderer.draw_rect(x, y, w, h, style.selection)
|
||||||
|
end
|
||||||
|
|
||||||
|
local mouse = self.mouse
|
||||||
|
if
|
||||||
|
mouse.x >= x
|
||||||
|
and
|
||||||
|
mouse.x <= x + w
|
||||||
|
and
|
||||||
|
mouse.y >= y
|
||||||
|
and
|
||||||
|
mouse.y <= y + h
|
||||||
|
then
|
||||||
|
renderer.draw_rect(x, y, w, h, style.line_highlight)
|
||||||
|
self.hovered_row = row
|
||||||
|
end
|
||||||
|
w, h = 0, 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- add padding on top
|
||||||
|
y = y + (style.padding.y / 2)
|
||||||
|
|
||||||
|
if #self.columns > 0 then
|
||||||
|
for col, coldata in ipairs(self.columns) do
|
||||||
|
-- padding on left
|
||||||
|
w = w + style.padding.x / 2
|
||||||
|
local cw, ch = self:draw_row_range(
|
||||||
|
row,
|
||||||
|
self.rows[row],
|
||||||
|
self.positions[row][col][1],
|
||||||
|
self.positions[row][col][2],
|
||||||
|
x + w,
|
||||||
|
y,
|
||||||
|
only_calc
|
||||||
|
)
|
||||||
|
-- add column width and end with padding on right
|
||||||
|
w = w + coldata.width + (style.padding.x / 2)
|
||||||
|
-- only store column height if bigger than previous one
|
||||||
|
h = math.max(h, ch)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local cw, ch = self:draw_row_range(
|
||||||
|
row,
|
||||||
|
self.rows[row],
|
||||||
|
1,
|
||||||
|
#self.rows[row],
|
||||||
|
x + style.padding.x / 2,
|
||||||
|
y,
|
||||||
|
only_calc
|
||||||
|
)
|
||||||
|
h = ch
|
||||||
|
w = cw + style.padding.x
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add padding on top and bottom
|
||||||
|
h = h + style.padding.y
|
||||||
|
|
||||||
|
if only_calc or not self.rows[row].w then
|
||||||
|
-- store the dimensions for inexpensive subsequent hover calculation
|
||||||
|
self.rows[row].w = w
|
||||||
|
self.rows[row].h = h
|
||||||
|
|
||||||
|
-- TODO: performance improvement, render only the visible rows on the view?
|
||||||
|
self.rows[row].x = x
|
||||||
|
self.rows[row].y = y - (style.padding.y / 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- return height with padding on top and bottom
|
||||||
|
return w, h
|
||||||
|
end
|
||||||
|
|
||||||
|
---
|
||||||
|
--- Events
|
||||||
|
---
|
||||||
|
|
||||||
|
function ListBox:on_mouse_leave(x, y, dx, dy)
|
||||||
|
ListBox.super.on_mouse_leave(self, x, y, dx, dy)
|
||||||
|
self.hovered_row = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function ListBox:on_mouse_moved(x, y, dx, dy)
|
||||||
|
ListBox.super.on_mouse_moved(self, x, y, dx, dy)
|
||||||
|
self.hovered_row = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function ListBox:on_click(button, x, y)
|
||||||
|
if button == "left" and self.hovered_row > 0 then
|
||||||
|
self.selected_row = self.hovered_row
|
||||||
|
self:on_row_click(self.hovered_row, self.row_data[self.hovered_row])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---You can overwrite this to listen to item clicks
|
||||||
|
---@param idx integer
|
||||||
|
---@param data any Data associated with the row
|
||||||
|
function ListBox:on_row_click(idx, data) end
|
||||||
|
|
||||||
|
function ListBox:update()
|
||||||
|
if not ListBox.super.update(self) then return false end
|
||||||
|
|
||||||
|
-- only calculate columns width on scale change since this can be expensive
|
||||||
|
if self.last_scale ~= SCALE then
|
||||||
|
if #self.columns > 0 then
|
||||||
|
for col, column in ipairs(self.columns) do
|
||||||
|
column.width = self:get_col_width(col)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self:recalc_all_rows()
|
||||||
|
self.last_scale = SCALE
|
||||||
|
end
|
||||||
|
|
||||||
|
local _, oy = self:get_content_offset()
|
||||||
|
if self.last_offset ~= oy then
|
||||||
|
self:set_visible_rows()
|
||||||
|
self.last_offset = oy
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function ListBox:draw()
|
||||||
|
if not ListBox.super.draw(self) then return false end
|
||||||
|
|
||||||
|
if #self.rows > 0 and #self.visible_rows <= 0 then
|
||||||
|
self:set_visible_rows()
|
||||||
|
end
|
||||||
|
|
||||||
|
local new_width = 0
|
||||||
|
local new_height = 0
|
||||||
|
local font = self:get_font()
|
||||||
|
|
||||||
|
if #self.columns > 0 then
|
||||||
|
new_height = new_height + font:get_height() + style.padding.y
|
||||||
|
for _, col in ipairs(self.columns) do
|
||||||
|
new_width = new_width + col.width + style.padding.x
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.expand then
|
||||||
|
self:resize_to_parent()
|
||||||
|
|
||||||
|
self.largest_row = self.size.x
|
||||||
|
- (self.parent.border.width * 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Normalize the offset position
|
||||||
|
local opx, opy = self.parent:get_content_offset()
|
||||||
|
if not self.parent.parent and not self.defer_draw then
|
||||||
|
-- TODO: inspect why is this workaround needed
|
||||||
|
-- Without it wrong offset occurs on child listviews of a single parent
|
||||||
|
-- like in the case of the recent projects in EmptyView.
|
||||||
|
opy = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local ox, oy = self:get_content_offset()
|
||||||
|
|
||||||
|
oy = oy - opy
|
||||||
|
if #self.visible_rows > 0 then
|
||||||
|
oy = oy + (self.rows[self.visible_rows[1]].y - new_height)
|
||||||
|
end
|
||||||
|
oy = oy - (self.position.y - self.parent.position.y)
|
||||||
|
|
||||||
|
ox = ox - opx
|
||||||
|
ox = ox - (self.position.x - self.parent.position.x)
|
||||||
|
|
||||||
|
local x = ox + self.position.x + self.border.width
|
||||||
|
local y = oy + self.position.y + self.border.width + new_height
|
||||||
|
|
||||||
|
core.push_clip_rect(
|
||||||
|
self.position.x, self.position.y, self.size.x, self.size.y
|
||||||
|
)
|
||||||
|
for _, ridx in ipairs(self.visible_rows) do
|
||||||
|
if self.rows[ridx] then
|
||||||
|
local w, h = self:draw_row(ridx, x, y)
|
||||||
|
new_width = math.max(new_width, w)
|
||||||
|
new_height = new_height + h
|
||||||
|
y = y + h
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not self.expand then
|
||||||
|
self.largest_row = self:get_width() - (self.border.width*2)
|
||||||
|
self.size.x = self.largest_row
|
||||||
|
end
|
||||||
|
|
||||||
|
if #self.columns > 0 then
|
||||||
|
self:draw_header(
|
||||||
|
self.largest_row,
|
||||||
|
font:get_height() + style.padding.y,
|
||||||
|
ox
|
||||||
|
)
|
||||||
|
end
|
||||||
|
core.pop_clip_rect()
|
||||||
|
|
||||||
|
self:draw_border()
|
||||||
|
self:draw_scrollbar()
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return ListBox
|
||||||
+343
@@ -0,0 +1,343 @@
|
|||||||
|
--
|
||||||
|
-- MessageBox Widget/Dialog.
|
||||||
|
-- @copyright Jefferson Gonzalez
|
||||||
|
-- @license MIT
|
||||||
|
--
|
||||||
|
|
||||||
|
local core = require "core"
|
||||||
|
local common = require "core.common"
|
||||||
|
local style = require "core.style"
|
||||||
|
local Widget = require "libraries.widget"
|
||||||
|
local Button = require "libraries.widget.button"
|
||||||
|
local Label = require "libraries.widget.label"
|
||||||
|
|
||||||
|
---@class widget.messagebox : widget
|
||||||
|
---@overload fun(parent?:widget, title?:string, message?:string|widget.styledtext, icon?:widget.messagebox.icontype, icon_color?:renderer.color):widget.messagebox
|
||||||
|
---@field private title widget.label
|
||||||
|
---@field private icon widget.label
|
||||||
|
---@field private message widget.label
|
||||||
|
---@field private buttons widget.button[]
|
||||||
|
local MessageBox = Widget:extend()
|
||||||
|
|
||||||
|
MessageBox.icon_huge_font = style.icon_font:copy(50 * SCALE)
|
||||||
|
|
||||||
|
MessageBox.ICON_ERROR = "X"
|
||||||
|
MessageBox.ICON_INFO = "i"
|
||||||
|
MessageBox.ICON_WARNING = "!"
|
||||||
|
|
||||||
|
---@alias widget.messagebox.icontype
|
||||||
|
---|>`MessageBox.ICON_ERROR`
|
||||||
|
---| `MessageBox.ICON_INFO`
|
||||||
|
---| `MessageBox.ICON_WARNING`
|
||||||
|
|
||||||
|
MessageBox.BUTTONS_OK = 1
|
||||||
|
MessageBox.BUTTONS_OK_CANCEL = 2
|
||||||
|
MessageBox.BUTTONS_YES_NO = 3
|
||||||
|
MessageBox.BUTTONS_YES_NO_CANCEL = 4
|
||||||
|
|
||||||
|
---@alias widget.messagebox.buttonstype
|
||||||
|
---|>`MessageBox.BUTTONS_OK`
|
||||||
|
---| `MessageBox.BUTTONS_OK_CANCEL`
|
||||||
|
---| `MessageBox.BUTTONS_YES_NO`
|
||||||
|
---| `MessageBox.BUTTONS_YES_NO_CANCEL`
|
||||||
|
|
||||||
|
---@alias widget.messagebox.onclosehandler fun(self: widget.messagebox, button_id: integer, button: widget.button)
|
||||||
|
|
||||||
|
---Constructor
|
||||||
|
---@param parent widget
|
||||||
|
---@param title string
|
||||||
|
---@param message string | widget.styledtext
|
||||||
|
---@param icon widget.messagebox.icontype
|
||||||
|
---@param icon_color renderer.color
|
||||||
|
function MessageBox:new(parent, title, message, icon, icon_color)
|
||||||
|
MessageBox.super.new(self, parent)
|
||||||
|
self.type_name = "widget.messagebox"
|
||||||
|
self.draggable = true
|
||||||
|
self.scrollable = true
|
||||||
|
self.title = Label(self, "")
|
||||||
|
self.icon = Label(self, "")
|
||||||
|
self.message = Label(self, "")
|
||||||
|
self.buttons = {}
|
||||||
|
self.last_scale = SCALE
|
||||||
|
|
||||||
|
self:set_title(title or "")
|
||||||
|
self:set_message(message or "")
|
||||||
|
self:set_icon(icon or "", icon_color)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Change the message box title.
|
||||||
|
---@param text string | widget.styledtext
|
||||||
|
function MessageBox:set_title(text)
|
||||||
|
self.title:set_label(text)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Change the message box icon.
|
||||||
|
---@param icon widget.messagebox.icontype
|
||||||
|
---@param color? renderer.color
|
||||||
|
function MessageBox:set_icon(icon, color)
|
||||||
|
if not color then
|
||||||
|
color = style.text
|
||||||
|
if icon == MessageBox.ICON_WARNING then
|
||||||
|
color = { common.color "#c7763e" }
|
||||||
|
elseif icon == MessageBox.ICON_ERROR then
|
||||||
|
color = { common.color "#c73e3e" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.icon:set_label({ MessageBox.icon_huge_font, color, icon })
|
||||||
|
end
|
||||||
|
|
||||||
|
---Change the message box message.
|
||||||
|
---@param text string | widget.styledtext
|
||||||
|
function MessageBox:set_message(text)
|
||||||
|
self.message:set_label(text)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Adds a new button to the message box.
|
||||||
|
---@param button_or_label string|widget.button
|
||||||
|
function MessageBox:add_button(button_or_label)
|
||||||
|
if type(button_or_label) == "table" then
|
||||||
|
table.insert(self.buttons, button_or_label)
|
||||||
|
else
|
||||||
|
local button = Button(self, button_or_label)
|
||||||
|
table.insert(self.buttons, button)
|
||||||
|
end
|
||||||
|
|
||||||
|
local button_id = #self.buttons
|
||||||
|
local new_button = self.buttons[button_id]
|
||||||
|
local on_click = new_button.on_click
|
||||||
|
new_button.on_click = function(this, ...)
|
||||||
|
on_click(this, ...)
|
||||||
|
self:on_close(button_id, new_button)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Calculate the width of all buttons combined.
|
||||||
|
---@return number width
|
||||||
|
function MessageBox:get_buttons_width()
|
||||||
|
local width = 0
|
||||||
|
if #self.buttons > 0 then
|
||||||
|
for _, button in ipairs(self.buttons) do
|
||||||
|
width = width + button:get_width()
|
||||||
|
end
|
||||||
|
-- add padding inbetween buttons
|
||||||
|
if #self.buttons > 1 then
|
||||||
|
width = width + ((style.padding.x) * (#self.buttons - 1))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return width
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the height of biggest button.
|
||||||
|
---@return number height
|
||||||
|
function MessageBox:get_buttons_height()
|
||||||
|
local height = 0
|
||||||
|
if #self.buttons > 0 then
|
||||||
|
for _, button in ipairs(self.buttons) do
|
||||||
|
height = math.max(height, button:get_height())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return height
|
||||||
|
end
|
||||||
|
|
||||||
|
---Position the buttons relative to the message.
|
||||||
|
function MessageBox:reposition_buttons()
|
||||||
|
local buttons_width = self:get_buttons_width()
|
||||||
|
local buttons_x = ((self.size.x / 2) - (buttons_width / 2))
|
||||||
|
local buttons_y = self.message:get_bottom() + (style.padding.y * 2)
|
||||||
|
if self.icon.label[3] ~= "" then
|
||||||
|
buttons_y = math.max(
|
||||||
|
buttons_y, self.icon:get_bottom() + (style.padding.y * 2)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, button in ipairs(self.buttons) do
|
||||||
|
button:set_position(buttons_x, buttons_y)
|
||||||
|
buttons_x = buttons_x + button:get_width() + style.padding.x
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Calculate the MessageBox size, centers it relative to screen and shows it.
|
||||||
|
function MessageBox:show()
|
||||||
|
MessageBox.super.show(self)
|
||||||
|
self:update()
|
||||||
|
self:centered()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Called when the user clicks one of the buttons in the message box.
|
||||||
|
---@param button_id integer
|
||||||
|
---@param button widget.button
|
||||||
|
---@diagnostic disable-next-line
|
||||||
|
function MessageBox:on_close(button_id, button) self:hide() end
|
||||||
|
|
||||||
|
function MessageBox:update()
|
||||||
|
if not MessageBox.super.update(self) then return false end
|
||||||
|
|
||||||
|
if self.last_scale ~= SCALE then
|
||||||
|
MessageBox.icon_huge_font = style.icon_font:copy(50 * SCALE)
|
||||||
|
self.last_scale = SCALE
|
||||||
|
self.icon.label[1] = MessageBox.icon_huge_font
|
||||||
|
elseif self.updated then
|
||||||
|
self.updated = true
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local width = math.max(self.title:get_width())
|
||||||
|
width = math.max(width, self.message:get_width() + self.icon:get_width())
|
||||||
|
width = math.max(width, self:get_buttons_width())
|
||||||
|
|
||||||
|
local height = self.title:get_height() + style.padding.y
|
||||||
|
if self.icon.label[3] == "" then
|
||||||
|
height = height + self.message:get_height() + (style.padding.y * 2)
|
||||||
|
else
|
||||||
|
height = height
|
||||||
|
+ math.max(self.icon:get_height(), self.message:get_height())
|
||||||
|
+ (style.padding.y * 2)
|
||||||
|
end
|
||||||
|
height = height + self:get_buttons_height()
|
||||||
|
|
||||||
|
self:set_size(width + style.padding.x * 2, height + style.padding.y * 2)
|
||||||
|
|
||||||
|
self.title:set_position(
|
||||||
|
style.padding.x / 2,
|
||||||
|
style.padding.y / 2
|
||||||
|
)
|
||||||
|
|
||||||
|
self.icon:set_position(
|
||||||
|
style.padding.x,
|
||||||
|
self.title:get_bottom() + style.padding.y
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.icon.label[3] == "" then
|
||||||
|
self.message:set_position(
|
||||||
|
style.padding.x,
|
||||||
|
self.title:get_bottom() + style.padding.y
|
||||||
|
)
|
||||||
|
else
|
||||||
|
local msg_y = self.title:get_bottom() + style.padding.y + 10
|
||||||
|
if self.icon:get_height() > self.message:get_height() then
|
||||||
|
msg_y = (self.icon:get_height() / 2)
|
||||||
|
- (self.message:get_height() / 2)
|
||||||
|
+ self.title:get_bottom() + style.padding.y
|
||||||
|
end
|
||||||
|
self.message:set_position(
|
||||||
|
self.icon:get_width() + (style.padding.x * 2) - (style.padding.x / 2),
|
||||||
|
msg_y
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
self:reposition_buttons()
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
---We overwrite default draw function to draw the title background.
|
||||||
|
function MessageBox:draw()
|
||||||
|
if not self:is_visible() then return false end
|
||||||
|
|
||||||
|
Widget.super.draw(self)
|
||||||
|
|
||||||
|
self:draw_border()
|
||||||
|
|
||||||
|
if self.background_color then
|
||||||
|
self:draw_background(self.background_color)
|
||||||
|
else
|
||||||
|
self:draw_background(
|
||||||
|
self.parent and style.background or style.background2
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
if #self.childs > 0 then
|
||||||
|
core.push_clip_rect(
|
||||||
|
self.position.x,
|
||||||
|
self.position.y,
|
||||||
|
self.size.x,
|
||||||
|
self.size.y
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- draw the title background
|
||||||
|
renderer.draw_rect(
|
||||||
|
self.position.x,
|
||||||
|
self.position.y,
|
||||||
|
self.size.x, self.title:get_height() + style.padding.y,
|
||||||
|
style.selection
|
||||||
|
)
|
||||||
|
|
||||||
|
for i=#self.childs, 1, -1 do
|
||||||
|
self.childs[i]:draw()
|
||||||
|
end
|
||||||
|
|
||||||
|
if #self.childs > 0 then
|
||||||
|
core.pop_clip_rect()
|
||||||
|
end
|
||||||
|
|
||||||
|
self:draw_scrollbar()
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
---Wrapper to easily show a message box.
|
||||||
|
---@param title string | widget.styledtext
|
||||||
|
---@param message string | widget.styledtext
|
||||||
|
---@param icon widget.messagebox.icontype
|
||||||
|
---@param icon_color? renderer.color
|
||||||
|
---@param on_close? widget.messagebox.onclosehandler
|
||||||
|
---@param buttons? widget.messagebox.buttonstype
|
||||||
|
function MessageBox.alert(title, message, icon, icon_color, on_close, buttons)
|
||||||
|
buttons = buttons or MessageBox.BUTTONS_OK
|
||||||
|
---@type widget.messagebox
|
||||||
|
local msgbox = MessageBox(nil, title, message, icon, icon_color)
|
||||||
|
if buttons == MessageBox.BUTTONS_OK_CANCEL then
|
||||||
|
msgbox:add_button("Ok")
|
||||||
|
msgbox:add_button("Cancel")
|
||||||
|
elseif buttons == MessageBox.BUTTONS_YES_NO then
|
||||||
|
msgbox:add_button("Yes")
|
||||||
|
msgbox:add_button("No")
|
||||||
|
elseif buttons == MessageBox.BUTTONS_YES_NO_CANCEL then
|
||||||
|
msgbox:add_button("Yes")
|
||||||
|
msgbox:add_button("No")
|
||||||
|
msgbox:add_button("Cancel")
|
||||||
|
else
|
||||||
|
msgbox:add_button("Ok")
|
||||||
|
end
|
||||||
|
|
||||||
|
local msgbox_on_close = msgbox.on_close
|
||||||
|
msgbox.on_close = function(self, button_id, button)
|
||||||
|
if on_close then
|
||||||
|
on_close(self, button_id, button)
|
||||||
|
end
|
||||||
|
msgbox_on_close(self, button_id, button)
|
||||||
|
self:destroy()
|
||||||
|
end
|
||||||
|
msgbox:show()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Wrapper to easily show a info message box.
|
||||||
|
---@param title string | widget.styledtext
|
||||||
|
---@param message string | widget.styledtext
|
||||||
|
---@param on_close? widget.messagebox.onclosehandler
|
||||||
|
---@param buttons? widget.messagebox.buttonstype
|
||||||
|
function MessageBox.info(title, message, on_close, buttons)
|
||||||
|
MessageBox.alert(title, message, MessageBox.ICON_INFO, nil, on_close, buttons)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Wrapper to easily show a warning message box.
|
||||||
|
---@param title string | widget.styledtext
|
||||||
|
---@param message string | widget.styledtext
|
||||||
|
---@param on_close? widget.messagebox.onclosehandler
|
||||||
|
---@param buttons? widget.messagebox.buttonstype
|
||||||
|
function MessageBox.warning(title, message, on_close, buttons)
|
||||||
|
MessageBox.alert(title, message, MessageBox.ICON_WARNING, nil, on_close, buttons)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Wrapper to easily show an error message box.
|
||||||
|
---@param title string | widget.styledtext
|
||||||
|
---@param message string | widget.styledtext
|
||||||
|
---@param on_close? widget.messagebox.onclosehandler
|
||||||
|
---@param buttons? widget.messagebox.buttonstype
|
||||||
|
function MessageBox.error(title, message, on_close, buttons)
|
||||||
|
MessageBox.alert(title, message, MessageBox.ICON_ERROR, nil, on_close, buttons)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return MessageBox
|
||||||
+188
@@ -0,0 +1,188 @@
|
|||||||
|
--
|
||||||
|
-- NoteBook Widget.
|
||||||
|
-- @copyright Jefferson Gonzalez
|
||||||
|
-- @license MIT
|
||||||
|
--
|
||||||
|
|
||||||
|
local style = require "core.style"
|
||||||
|
local Widget = require "libraries.widget"
|
||||||
|
local Button = require "libraries.widget.button"
|
||||||
|
|
||||||
|
local HSPACING = 2
|
||||||
|
local VSPACING = 2
|
||||||
|
|
||||||
|
---Represents a notebook pane
|
||||||
|
---@class widget.notebook.pane
|
||||||
|
---@field public name string
|
||||||
|
---@field public tab widget.button
|
||||||
|
---@field public container widget
|
||||||
|
local NoteBookPane = {}
|
||||||
|
|
||||||
|
---@class widget.notebook : widget
|
||||||
|
---@overload fun(parent?:widget):widget.notebook
|
||||||
|
---@field public panes widget.notebook.pane[]
|
||||||
|
---@field public active_pane widget.notebook.pane
|
||||||
|
local NoteBook = Widget:extend()
|
||||||
|
|
||||||
|
---Notebook constructor
|
||||||
|
---@param parent widget
|
||||||
|
function NoteBook:new(parent)
|
||||||
|
NoteBook.super.new(self, parent)
|
||||||
|
self.type_name = "widget.notebook"
|
||||||
|
self.panes = {}
|
||||||
|
self.active_pane = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---Called when a tab is clicked.
|
||||||
|
---@param pane widget.notebook.pane
|
||||||
|
---@diagnostic disable-next-line
|
||||||
|
function NoteBook:on_tab_click(pane) end
|
||||||
|
|
||||||
|
---Adds a new pane to the notebook and returns a container widget where
|
||||||
|
---you can add more child elements.
|
||||||
|
---@param name string
|
||||||
|
---@param label string
|
||||||
|
---@return widget container
|
||||||
|
function NoteBook:add_pane(name, label)
|
||||||
|
---@type widget.button
|
||||||
|
local tab = Button(self, label)
|
||||||
|
tab.border.width = 0
|
||||||
|
|
||||||
|
if #self.panes > 0 then
|
||||||
|
tab:set_position(self.panes[#self.panes].tab:get_right() + HSPACING, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
local container = Widget(self)
|
||||||
|
container.scrollable = true
|
||||||
|
container:set_position(0, tab:get_bottom() + VSPACING)
|
||||||
|
container:set_size(
|
||||||
|
self:get_width(),
|
||||||
|
self:get_height() - tab:get_height() - VSPACING
|
||||||
|
)
|
||||||
|
|
||||||
|
local pane = {
|
||||||
|
name = name,
|
||||||
|
tab = tab,
|
||||||
|
container = container
|
||||||
|
}
|
||||||
|
|
||||||
|
if not self.active_pane then
|
||||||
|
self.active_pane = pane
|
||||||
|
end
|
||||||
|
|
||||||
|
tab.on_click = function()
|
||||||
|
self.active_pane = pane
|
||||||
|
self:on_tab_click(pane)
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(self.panes, pane)
|
||||||
|
|
||||||
|
return container
|
||||||
|
end
|
||||||
|
|
||||||
|
---Search the pane for the given name and return it.
|
||||||
|
---@param name string
|
||||||
|
---@return widget.notebook.pane | nil
|
||||||
|
function NoteBook:get_pane(name)
|
||||||
|
for _, pane in pairs(self.panes) do
|
||||||
|
if pane.name == name then
|
||||||
|
return pane
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---Activates the given pane.
|
||||||
|
---@param name string
|
||||||
|
---@return boolean
|
||||||
|
function NoteBook:set_pane(name)
|
||||||
|
local pane = self:get_pane(name)
|
||||||
|
if pane then
|
||||||
|
self.active_pane = pane
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
---Change the tab label of the given pane.
|
||||||
|
---@param name string
|
||||||
|
---@param label string
|
||||||
|
function NoteBook:set_pane_label(name, label)
|
||||||
|
local pane = self:get_pane(name)
|
||||||
|
if pane then
|
||||||
|
pane.tab:set_label(label)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set or remove the icon for the given pane.
|
||||||
|
---@param name string
|
||||||
|
---@param icon? string|nil
|
||||||
|
---@param color? renderer.color|nil
|
||||||
|
---@param hover_color? renderer.color|nil
|
||||||
|
function NoteBook:set_pane_icon(name, icon, color, hover_color)
|
||||||
|
local pane = self:get_pane(name)
|
||||||
|
if pane then
|
||||||
|
pane.tab:set_icon(icon, color, hover_color)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
---Recalculate the position of the elements on resizing or position
|
||||||
|
---changes and also make changes to properly render active pane.
|
||||||
|
function NoteBook:update()
|
||||||
|
if not NoteBook.super.update(self) then return false end
|
||||||
|
|
||||||
|
for pos, pane in pairs(self.panes) do
|
||||||
|
if pos ~= 1 then
|
||||||
|
pane.tab:set_position(
|
||||||
|
self.panes[pos-1].tab:get_right() + HSPACING, 0
|
||||||
|
)
|
||||||
|
else
|
||||||
|
pane.tab:set_position(0, 0)
|
||||||
|
end
|
||||||
|
if pane ~= self.active_pane then
|
||||||
|
if pane.container.visible then
|
||||||
|
pane.tab.background_color = style.background
|
||||||
|
pane.tab.foreground_color = style.text
|
||||||
|
pane.container:hide()
|
||||||
|
end
|
||||||
|
elseif not pane.container.visible then
|
||||||
|
pane.container:show()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.active_pane then
|
||||||
|
self.active_pane.tab.foreground_color = style.accent
|
||||||
|
|
||||||
|
self.active_pane.container:set_position(
|
||||||
|
0, self.active_pane.tab:get_bottom() + VSPACING
|
||||||
|
)
|
||||||
|
self.active_pane.container:set_size(
|
||||||
|
self:get_width(),
|
||||||
|
self:get_height() - self.active_pane.tab:get_height() - VSPACING
|
||||||
|
)
|
||||||
|
self.active_pane.container.border.color = style.divider
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
---Here we draw the bottom line on the tab of active pane.
|
||||||
|
function NoteBook:draw()
|
||||||
|
if not NoteBook.super.draw(self) then return false end
|
||||||
|
|
||||||
|
if self.active_pane then
|
||||||
|
local x = self.active_pane.tab.position.x
|
||||||
|
local y = self.active_pane.tab.position.y + self.active_pane.tab:get_bottom()
|
||||||
|
local w = self.active_pane.tab:get_width()
|
||||||
|
renderer.draw_rect(x, y, w, HSPACING, style.caret)
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return NoteBook
|
||||||
+238
@@ -0,0 +1,238 @@
|
|||||||
|
--
|
||||||
|
-- NumberBox Widget.
|
||||||
|
-- @copyright Jefferson Gonzalez
|
||||||
|
-- @license MIT
|
||||||
|
--
|
||||||
|
|
||||||
|
local core = require "core"
|
||||||
|
local Widget = require "libraries.widget"
|
||||||
|
local Button = require "libraries.widget.button"
|
||||||
|
local TextBox = require "libraries.widget.textbox"
|
||||||
|
|
||||||
|
---@class widget.numberbox : widget
|
||||||
|
---@overload fun(parent?:widget, value:number, min?:number, max?:number, step?:number):widget.numberbox
|
||||||
|
---@field private textbox widget.textbox
|
||||||
|
---@field private decrease_button widget.button
|
||||||
|
---@field private increase_button widget.button
|
||||||
|
---@field private minimum number
|
||||||
|
---@field private maximum number
|
||||||
|
---@field private step number
|
||||||
|
local NumberBox = Widget:extend()
|
||||||
|
|
||||||
|
---Constructor
|
||||||
|
---@param parent widget
|
||||||
|
---@param value number
|
||||||
|
---@param min? number
|
||||||
|
---@param max? number
|
||||||
|
---@param step? number
|
||||||
|
function NumberBox:new(parent, value, min, max, step)
|
||||||
|
NumberBox.super.new(self, parent)
|
||||||
|
|
||||||
|
self.type_name = "widget.numberbox"
|
||||||
|
|
||||||
|
self:set_range(min, max)
|
||||||
|
self:set_step(step)
|
||||||
|
|
||||||
|
self.textbox = TextBox(self, "")
|
||||||
|
self.textbox.scrollable = true
|
||||||
|
|
||||||
|
self.decrease_button = Button(self, "-")
|
||||||
|
self.increase_button = Button(self, "+")
|
||||||
|
|
||||||
|
self:set_value(value)
|
||||||
|
|
||||||
|
local this = self
|
||||||
|
function self.textbox.textview.doc:on_text_change(type)
|
||||||
|
if not tonumber(this.textbox:get_text()) then
|
||||||
|
if not this.coroutine_run then
|
||||||
|
this.coroutine_run = true
|
||||||
|
core.add_thread(function()
|
||||||
|
this.textbox:set_text(this.current_text)
|
||||||
|
this.coroutine_run = false
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
this.textbox.placeholder_active = false
|
||||||
|
this.current_text = this.textbox:get_text()
|
||||||
|
this:on_change(tonumber(this.current_text))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function self.textbox:on_mouse_wheel(y)
|
||||||
|
if self.active then
|
||||||
|
if y > 0 then this:increase() else this:decrease() end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
function self.decrease_button:on_mouse_pressed(button, x, y, clicks)
|
||||||
|
if Button.super.on_mouse_pressed(self, button, x, y, clicks) then
|
||||||
|
this.mouse_is_pressed = true
|
||||||
|
this:mouse_pressed(false)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
function self.increase_button:on_mouse_pressed(button, x, y, clicks)
|
||||||
|
if Button.super.on_mouse_pressed(self, button, x, y, clicks) then
|
||||||
|
this.mouse_is_pressed = true
|
||||||
|
this:mouse_pressed(true)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
function self.decrease_button:on_mouse_released(button, x, y)
|
||||||
|
if Button.super.on_mouse_released(self, button, x, y) then
|
||||||
|
this.mouse_is_pressed = false
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
function self.increase_button:on_mouse_released(button, x, y)
|
||||||
|
if Button.super.on_mouse_released(self, button, x, y) then
|
||||||
|
this.mouse_is_pressed = false
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
self.border.width = 0
|
||||||
|
|
||||||
|
self:set_size(
|
||||||
|
self.textbox:get_width() - 100
|
||||||
|
+ self.decrease_button:get_width()
|
||||||
|
+ self.increase_button:get_width()
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set a new value.
|
||||||
|
---@param value number
|
||||||
|
function NumberBox:set_value(value)
|
||||||
|
if type(value) == "number" then
|
||||||
|
self.textbox:set_text(tostring(value))
|
||||||
|
elseif type(value) == "string" and tonumber(value) then
|
||||||
|
self.textbox:set_text(value)
|
||||||
|
else
|
||||||
|
self.textbox:set_text(tostring(self.minimum))
|
||||||
|
end
|
||||||
|
self.textbox.placeholder_active = false
|
||||||
|
self.current_text = self.textbox:get_text()
|
||||||
|
self:on_change(tonumber(self.current_text))
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the current value.
|
||||||
|
---@return number
|
||||||
|
function NumberBox:get_value()
|
||||||
|
return tonumber(self.textbox:get_text()) or self.minimum
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set the minimum and maximum values allowed.
|
||||||
|
---@param min? number
|
||||||
|
---@param max? number
|
||||||
|
function NumberBox:set_range(min, max)
|
||||||
|
self.minimum = min or math.mininteger
|
||||||
|
self.maximum = max or math.maxinteger
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set the value used to increase or decrease the number when the
|
||||||
|
---buttons are pressed.
|
||||||
|
---@param step number
|
||||||
|
function NumberBox:set_step(step)
|
||||||
|
self.step = step or 1
|
||||||
|
end
|
||||||
|
|
||||||
|
---Decrease the current value.
|
||||||
|
function NumberBox:decrease()
|
||||||
|
self.textbox.placeholder_active = false
|
||||||
|
local value = tonumber(self.textbox:get_text()) or self.maximum
|
||||||
|
if (value - self.step) >= self.minimum then
|
||||||
|
self:set_value(value - self.step)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Increase the current value.
|
||||||
|
function NumberBox:increase()
|
||||||
|
self.textbox.placeholder_active = false
|
||||||
|
local value = tonumber(self.textbox:get_text()) or self.minimum
|
||||||
|
if (value + self.step) <= self.maximum then
|
||||||
|
self:set_value(value + self.step)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Triggered when the mouse is pressed on the increase/decrease buttons.
|
||||||
|
---@param increase boolean
|
||||||
|
function NumberBox:mouse_pressed(increase)
|
||||||
|
if increase then self:increase() else self:decrease() end
|
||||||
|
|
||||||
|
local elapsed = system.get_time() + 0.3
|
||||||
|
local this = self
|
||||||
|
|
||||||
|
core.add_thread(function()
|
||||||
|
while this.mouse_is_pressed do
|
||||||
|
if elapsed < system.get_time() then
|
||||||
|
if increase then
|
||||||
|
this:increase()
|
||||||
|
else
|
||||||
|
this:decrease()
|
||||||
|
end
|
||||||
|
core.redraw = true
|
||||||
|
elapsed = system.get_time() + 0.1
|
||||||
|
end
|
||||||
|
coroutine.yield()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Overrided to enforce minimum allowed size.
|
||||||
|
---@param width integer
|
||||||
|
---@param height? integer Ignored on the number box
|
||||||
|
function NumberBox:set_size(width, height)
|
||||||
|
local buttons_w = self.decrease_button:get_width()
|
||||||
|
+ self.increase_button:get_width()
|
||||||
|
|
||||||
|
-- permit a minimum of 100 pixels wide for textbox
|
||||||
|
if width < (buttons_w + 100) then
|
||||||
|
width = 100 + buttons_w
|
||||||
|
end
|
||||||
|
|
||||||
|
self.textbox:set_size(width - buttons_w)
|
||||||
|
|
||||||
|
NumberBox.super.set_size(
|
||||||
|
self,
|
||||||
|
width,
|
||||||
|
math.max(
|
||||||
|
self.textbox:get_height(),
|
||||||
|
self.decrease_button:get_height(),
|
||||||
|
self.increase_button:get_height()
|
||||||
|
)
|
||||||
|
-- TODO: check what causes the need for this border size addition since
|
||||||
|
-- it shouldn't be needed but for now fixes occasional bottom border cut.
|
||||||
|
+ self.increase_button.border.width
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function NumberBox:update()
|
||||||
|
if not NumberBox.super.update(self) then return false end
|
||||||
|
|
||||||
|
self:set_size(
|
||||||
|
self.textbox:get_width()
|
||||||
|
+ self.decrease_button:get_width()
|
||||||
|
+ self.increase_button:get_width()
|
||||||
|
)
|
||||||
|
|
||||||
|
self.textbox:set_position(0, 0)
|
||||||
|
|
||||||
|
self.decrease_button:set_position(
|
||||||
|
self.textbox:get_right(),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
|
self.increase_button:set_position(
|
||||||
|
self.decrease_button:get_right(),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return NumberBox
|
||||||
+95
@@ -0,0 +1,95 @@
|
|||||||
|
--
|
||||||
|
-- ProgressBar Widget.
|
||||||
|
-- @copyright Jefferson Gonzalez
|
||||||
|
-- @license MIT
|
||||||
|
--
|
||||||
|
|
||||||
|
local style = require "core.style"
|
||||||
|
local Widget = require "libraries.widget"
|
||||||
|
|
||||||
|
---@class widget.progressbar : widget
|
||||||
|
---@overload fun(parent?:widget, percent?:number, width?:number):widget.progressbar
|
||||||
|
---@field public percent number
|
||||||
|
---@field public show_percent boolean
|
||||||
|
---@field private percent_width number
|
||||||
|
---@field private percent_x number
|
||||||
|
---@field private percent_y number
|
||||||
|
local ProgressBar = Widget:extend()
|
||||||
|
|
||||||
|
---Constructor
|
||||||
|
---@param parent widget
|
||||||
|
---@param percent number
|
||||||
|
---@param width number
|
||||||
|
function ProgressBar:new(parent, percent, width)
|
||||||
|
ProgressBar.super.new(self, parent)
|
||||||
|
self.type_name = "widget.progressbar"
|
||||||
|
self.clickable = false
|
||||||
|
self.percent = percent or 0
|
||||||
|
self.percent_width = 0
|
||||||
|
self.show_percent = true
|
||||||
|
self:set_size(width or 200, self:get_font():get_height() + style.padding.y)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param percent number
|
||||||
|
function ProgressBar:set_percent(percent)
|
||||||
|
self.percent = percent
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return number
|
||||||
|
function ProgressBar:get_percent()
|
||||||
|
return self.percent
|
||||||
|
end
|
||||||
|
|
||||||
|
function ProgressBar:update()
|
||||||
|
if not ProgressBar.super.update(self) then return false end
|
||||||
|
|
||||||
|
local font = self:get_font()
|
||||||
|
|
||||||
|
-- update the size
|
||||||
|
self:set_label(self.percent .. "%")
|
||||||
|
|
||||||
|
self:set_size(
|
||||||
|
nil,
|
||||||
|
font:get_height() + style.padding.y
|
||||||
|
)
|
||||||
|
|
||||||
|
local percent_width = (self.size.x * (self.percent / 100))
|
||||||
|
|
||||||
|
self:move_towards(self, "percent_width", percent_width, 0.2)
|
||||||
|
|
||||||
|
if self.show_percent then
|
||||||
|
self.percent_x = (self:get_width() / 2)
|
||||||
|
- (font:get_width(self.label) / 2)
|
||||||
|
|
||||||
|
self.percent_y = style.padding.y / 2
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function ProgressBar:draw()
|
||||||
|
if not ProgressBar.super.draw(self) then return false end
|
||||||
|
|
||||||
|
renderer.draw_rect(
|
||||||
|
self.position.x,
|
||||||
|
self.position.y,
|
||||||
|
self.percent_width,
|
||||||
|
self.size.y,
|
||||||
|
style.dim
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.show_percent then
|
||||||
|
renderer.draw_text(
|
||||||
|
self:get_font(),
|
||||||
|
self.label,
|
||||||
|
self.position.x + self.percent_x,
|
||||||
|
self.position.y + self.percent_y,
|
||||||
|
style.text
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return ProgressBar
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
--
|
||||||
|
-- Extends core.scrollbar to allow propagating force status to child elements.
|
||||||
|
--
|
||||||
|
|
||||||
|
---@type core.scrollbar
|
||||||
|
local CoreScrollBar = require "core.scrollbar"
|
||||||
|
|
||||||
|
---@class widget.scrollbar : core.scrollbar
|
||||||
|
---@overload fun(parent?:widget, options?:table):widget.scrollbar
|
||||||
|
---@field super widget.scrollbar
|
||||||
|
---@field widget_parent widget
|
||||||
|
local ScrollBar = CoreScrollBar:extend()
|
||||||
|
|
||||||
|
function ScrollBar:new(parent, options)
|
||||||
|
self.widget_parent = parent
|
||||||
|
ScrollBar.super.new(self, options)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ScrollBar:set_forced_status(status)
|
||||||
|
ScrollBar.super.set_forced_status(self, status)
|
||||||
|
if self.widget_parent and self.widget_parent.childs then
|
||||||
|
for _, child in pairs(self.widget_parent.childs) do
|
||||||
|
if self.direction == "v" then
|
||||||
|
child.v_scrollbar:set_forced_status(status)
|
||||||
|
else
|
||||||
|
child.h_scrollbar:set_forced_status(status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return ScrollBar
|
||||||
+607
@@ -0,0 +1,607 @@
|
|||||||
|
--
|
||||||
|
-- SearchReplaceList Widget.
|
||||||
|
-- @copyright Jefferson Gonzalez
|
||||||
|
-- @license MIT
|
||||||
|
--
|
||||||
|
|
||||||
|
local core = require "core"
|
||||||
|
local common = require "core.common"
|
||||||
|
local style = require "core.style"
|
||||||
|
local Widget = require "libraries.widget"
|
||||||
|
|
||||||
|
---@class widget.searchreplacelist.lineposition
|
||||||
|
---@field col1 integer
|
||||||
|
---@field col2 integer
|
||||||
|
---@field checked boolean?
|
||||||
|
|
||||||
|
---@class widget.searchreplacelist.line
|
||||||
|
---@field line integer
|
||||||
|
---@field text string
|
||||||
|
---@field positions widget.searchreplacelist.lineposition[]
|
||||||
|
|
||||||
|
---@class widget.searchreplacelist.file
|
||||||
|
---@field path string
|
||||||
|
---@field lines widget.searchreplacelist.line[]
|
||||||
|
---@field expanded boolean
|
||||||
|
|
||||||
|
---@class widget.searchreplacelist.item
|
||||||
|
---@field checked boolean
|
||||||
|
---@field file widget.searchreplacelist.file?
|
||||||
|
---@field parent widget.searchreplacelist.item?
|
||||||
|
---@field line widget.searchreplacelist.line?
|
||||||
|
---@field position widget.searchreplacelist.lineposition?
|
||||||
|
|
||||||
|
---@class widget.searchreplacelist : widget
|
||||||
|
---@field replacement string?
|
||||||
|
---@field selected integer
|
||||||
|
---@field hovered integer
|
||||||
|
---@field items widget.searchreplacelist.item[]
|
||||||
|
---@field total_files integer
|
||||||
|
---@field total_results integer
|
||||||
|
---@field base_dir string
|
||||||
|
---@overload fun(parent:widget, find:string, replacement:string?):widget.searchreplacelist
|
||||||
|
local SearchReplaceList = Widget:extend()
|
||||||
|
|
||||||
|
---Colors to use when performing a replacement.
|
||||||
|
local DIFF = {
|
||||||
|
ADD = { common.color "#72b886" },
|
||||||
|
DEL = { common.color "#F36161" },
|
||||||
|
TEXT = { common.color "#000000" }
|
||||||
|
}
|
||||||
|
|
||||||
|
---Constructor
|
||||||
|
---@param parent widget
|
||||||
|
---@param replacement string?
|
||||||
|
function SearchReplaceList:new(parent, replacement)
|
||||||
|
SearchReplaceList.super.new(self, parent)
|
||||||
|
|
||||||
|
self.type_name = "widget.searchreplacelist"
|
||||||
|
|
||||||
|
self.replacement = replacement or nil
|
||||||
|
self.selected = 0
|
||||||
|
self.hovered = 0
|
||||||
|
self.items = {}
|
||||||
|
self.max_width = 0
|
||||||
|
self.hovered_checkbox = false
|
||||||
|
self.hovered_expander = false
|
||||||
|
self.scrollable = true
|
||||||
|
self.total_files = 0
|
||||||
|
self.total_results = 0
|
||||||
|
self.base_dir = ""
|
||||||
|
end
|
||||||
|
|
||||||
|
---Overridable event triggered when an item is clicked.
|
||||||
|
---@param item widget.searchreplacelist.item
|
||||||
|
---@param clicks integer
|
||||||
|
function SearchReplaceList:on_item_click(item, clicks) end
|
||||||
|
|
||||||
|
---Add a new file with all the matching lines and positions.
|
||||||
|
---@param path string
|
||||||
|
---@param lines widget.searchreplacelist.line[]
|
||||||
|
---@param expanded boolean?
|
||||||
|
function SearchReplaceList:add_file(path, lines, expanded)
|
||||||
|
if type(expanded) == "nil" then expanded = true end
|
||||||
|
table.insert(self.items, {
|
||||||
|
checked = true,
|
||||||
|
file = {
|
||||||
|
path = path,
|
||||||
|
lines = lines,
|
||||||
|
expanded = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
self.total_files = self.total_files + 1
|
||||||
|
-- expand results and count them
|
||||||
|
if expanded and #lines > 0 then
|
||||||
|
self:expand(#self.items, true)
|
||||||
|
-- count the results only
|
||||||
|
else
|
||||||
|
for _, line in ipairs(self.items[#self.items]) do
|
||||||
|
self.total_results = self.total_results + #line.positions
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Removes all elements from the list and reset it.
|
||||||
|
function SearchReplaceList:clear()
|
||||||
|
self.selected = 0
|
||||||
|
self.hovered = 0
|
||||||
|
self.items = {}
|
||||||
|
self.max_width = 0
|
||||||
|
self.hovered_checkbox = false
|
||||||
|
self.hovered_expander = false
|
||||||
|
self.total_files = 0
|
||||||
|
self.total_results = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
---Uncollapse a file line results.
|
||||||
|
---@param position integer
|
||||||
|
function SearchReplaceList:expand(position, count_results)
|
||||||
|
local parent = self.items[position]
|
||||||
|
if
|
||||||
|
parent and parent.file
|
||||||
|
and
|
||||||
|
not parent.file.expanded
|
||||||
|
then
|
||||||
|
local insert_pos = position+1
|
||||||
|
local items = {}
|
||||||
|
for _, line in ipairs(parent.file.lines) do
|
||||||
|
for _, line_pos in ipairs(line.positions) do
|
||||||
|
table.insert(items, {
|
||||||
|
parent = parent,
|
||||||
|
line = line,
|
||||||
|
position = line_pos
|
||||||
|
})
|
||||||
|
if count_results then
|
||||||
|
self.total_results = self.total_results + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
common.splice(self.items, insert_pos, 0, items)
|
||||||
|
parent.file.expanded = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Collapse a file line results.
|
||||||
|
---@param position integer
|
||||||
|
function SearchReplaceList:contract(position)
|
||||||
|
local parent = self.items[position]
|
||||||
|
if
|
||||||
|
parent and parent.file
|
||||||
|
and
|
||||||
|
parent.file.expanded
|
||||||
|
then
|
||||||
|
local start_pos = position+1
|
||||||
|
local end_pos = 0
|
||||||
|
for _, line in ipairs(parent.file.lines) do
|
||||||
|
for _, _ in ipairs(line.positions) do
|
||||||
|
end_pos = end_pos + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
common.splice(self.items, start_pos, end_pos)
|
||||||
|
parent.file.expanded = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Collapse/uncollapse a file line results.
|
||||||
|
---@param position integer
|
||||||
|
function SearchReplaceList:toggle_expand(position)
|
||||||
|
local parent = self.items[position]
|
||||||
|
if parent and parent.file then
|
||||||
|
if parent.file.expanded then
|
||||||
|
self:contract(position)
|
||||||
|
else
|
||||||
|
self:expand(position)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Toggle the checkbox of the given item position.
|
||||||
|
---@param pos integer
|
||||||
|
function SearchReplaceList:toggle_check(pos)
|
||||||
|
local item = self.items[pos]
|
||||||
|
if not item or self.replacement == nil then return end
|
||||||
|
if item.file then
|
||||||
|
item.checked = not item.checked
|
||||||
|
for _, line in ipairs(item.file.lines) do
|
||||||
|
for _, position in ipairs(line.positions) do
|
||||||
|
position.checked = item.checked
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if type(item.position.checked) == "nil" then
|
||||||
|
item.position.checked = false
|
||||||
|
else
|
||||||
|
item.position.checked = not item.position.checked
|
||||||
|
end
|
||||||
|
local parent_checked = true
|
||||||
|
for _, line in ipairs(item.parent.file.lines) do
|
||||||
|
for _, position in ipairs(line.positions) do
|
||||||
|
if position.checked == false then
|
||||||
|
parent_checked = false
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
item.parent.checked = parent_checked
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Replace a position on a string with a given replacement.
|
||||||
|
---@param str string
|
||||||
|
---@param s integer
|
||||||
|
---@param e integer
|
||||||
|
---@param rep string
|
||||||
|
local function replace_substring(str, s, e, rep)
|
||||||
|
local head = s <= 1 and "" or string.sub(str, 1, s - 1)
|
||||||
|
local tail = e >= #str and "" or string.sub(str, e + 1)
|
||||||
|
return head .. rep .. tail
|
||||||
|
end
|
||||||
|
|
||||||
|
---Applies the replacement on the given item position but not on the real file.
|
||||||
|
---
|
||||||
|
---The purpose of this function is to reflect the changes on the listed items.
|
||||||
|
---@param position integer
|
||||||
|
function SearchReplaceList:apply_replacement(position)
|
||||||
|
local item = self.items[position]
|
||||||
|
if item and item.file and self.replacement then
|
||||||
|
local replacement = self.replacement
|
||||||
|
local replacement_len = #self.replacement
|
||||||
|
for _, line in ipairs(item.file.lines) do
|
||||||
|
local offset = 0
|
||||||
|
for _, pos in ipairs(line.positions) do
|
||||||
|
local col1 = pos.col1 + offset
|
||||||
|
local col2 = pos.col2 + offset
|
||||||
|
if pos.checked or type(pos.checked) == "nil" then
|
||||||
|
line.text = replace_substring(line.text, col1, col2, replacement)
|
||||||
|
local current_len = col2 - col1 + 1
|
||||||
|
local len_diff = 0
|
||||||
|
if current_len > replacement_len then
|
||||||
|
len_diff = current_len - replacement_len
|
||||||
|
offset = offset - len_diff
|
||||||
|
col2 = col2 - len_diff
|
||||||
|
elseif current_len < replacement_len then
|
||||||
|
len_diff = replacement_len - current_len
|
||||||
|
offset = offset + len_diff
|
||||||
|
col2 = col2 + len_diff
|
||||||
|
end
|
||||||
|
end
|
||||||
|
pos.col1, pos.col2 = col1, col2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Select the item that follows currently selected item.
|
||||||
|
---@return widget.searchreplacelist.item?
|
||||||
|
function SearchReplaceList:select_next()
|
||||||
|
local items_count = #self.items
|
||||||
|
if items_count <= 0 then return nil end
|
||||||
|
local selected = self.selected+1
|
||||||
|
if selected > items_count then selected = 1 end
|
||||||
|
self.selected = common.clamp(selected, 1, items_count)
|
||||||
|
self:scroll_to_selected()
|
||||||
|
return self.items[self.selected]
|
||||||
|
end
|
||||||
|
|
||||||
|
---Select the item that precedes currently selected item.
|
||||||
|
---@return widget.searchreplacelist.item?
|
||||||
|
function SearchReplaceList:select_prev()
|
||||||
|
local items_count = #self.items
|
||||||
|
if items_count <= 0 then return nil end
|
||||||
|
local selected = self.selected-1
|
||||||
|
if selected < 1 then selected = items_count end
|
||||||
|
self.selected = common.clamp(selected, 1, items_count)
|
||||||
|
self:scroll_to_selected()
|
||||||
|
return self.items[self.selected]
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the currently selected item.
|
||||||
|
---@return widget.searchreplacelist.item?
|
||||||
|
function SearchReplaceList:get_selected()
|
||||||
|
if self.selected > 0 and self.selected <= #self.items then
|
||||||
|
return self.items[self.selected]
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the line height used when drawing each item row.
|
||||||
|
---@return number height
|
||||||
|
function SearchReplaceList:get_line_height()
|
||||||
|
return style.padding.y + style.font:get_height()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Used when calculating if vertical scrolling is needed.
|
||||||
|
---@return number size
|
||||||
|
function SearchReplaceList:get_scrollable_size()
|
||||||
|
return #self.items * self:get_line_height() + style.padding.y
|
||||||
|
end
|
||||||
|
|
||||||
|
---Used when calculating if horizontal scrolling is needed.
|
||||||
|
---@return number size
|
||||||
|
function SearchReplaceList:get_h_scrollable_size()
|
||||||
|
local width = style.padding.x / 2
|
||||||
|
+ style.icon_font:get_width("-")
|
||||||
|
+ style.padding.x / 2
|
||||||
|
+ self.max_width
|
||||||
|
if self.replacement then
|
||||||
|
local cb_w = self:get_checkbox_size()
|
||||||
|
width = width
|
||||||
|
+ cb_w + style.padding.x / 2
|
||||||
|
+ style.font:get_width(self.replacement)
|
||||||
|
end
|
||||||
|
return width
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the checkbox size based on the line height.
|
||||||
|
---@param y? number
|
||||||
|
---@return number w
|
||||||
|
---@return number h
|
||||||
|
---@return number y Vertically center align coord based on given y param
|
||||||
|
function SearchReplaceList:get_checkbox_size(y)
|
||||||
|
if not y then y = 0 end
|
||||||
|
local lh = self:get_line_height()
|
||||||
|
local w = lh * 0.6
|
||||||
|
local h = w
|
||||||
|
y = y + ((lh / 2) - (h / 2))
|
||||||
|
return w, h, y
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the position of first and last visible items.
|
||||||
|
---@return integer first
|
||||||
|
---@return integer last
|
||||||
|
function SearchReplaceList:get_visible_items_range()
|
||||||
|
local lh = self:get_line_height()
|
||||||
|
local min = math.max(1, math.floor(self.scroll.y / lh))
|
||||||
|
return min, min + math.floor(self.size.y / lh) + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
---Allows iterating the currently visible items only.
|
||||||
|
---@return fun():integer,widget.searchreplacelist.item,number,number,number,number
|
||||||
|
function SearchReplaceList:each_visible_item()
|
||||||
|
return coroutine.wrap(function()
|
||||||
|
local lh = self:get_line_height()
|
||||||
|
local x, y = self:get_content_offset()
|
||||||
|
local min, max = self:get_visible_items_range()
|
||||||
|
y = y + lh * (min - 1)
|
||||||
|
for i = min, max do
|
||||||
|
local item = self.items[i]
|
||||||
|
if not item then break end
|
||||||
|
coroutine.yield(i, item, x, y, self.size.x, lh)
|
||||||
|
y = y + lh
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Iterates over all files, only those that have a position checked on replacement mode.
|
||||||
|
---@return fun():integer,widget.searchreplacelist.file
|
||||||
|
function SearchReplaceList:each_file()
|
||||||
|
return coroutine.wrap(function()
|
||||||
|
local replacement = self.replacement
|
||||||
|
for i, item in ipairs(self.items) do
|
||||||
|
if item.file then
|
||||||
|
if replacement then
|
||||||
|
local none_checked = true
|
||||||
|
for _, line in ipairs(item.file.lines) do
|
||||||
|
for _, pos in ipairs(line.positions) do
|
||||||
|
if type(pos.checked) == "nil" or pos.checked then
|
||||||
|
none_checked = false
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if none_checked then
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
end
|
||||||
|
coroutine.yield(i, item.file)
|
||||||
|
end
|
||||||
|
::continue::
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Scroll to currently selected item only if not already visible.
|
||||||
|
function SearchReplaceList:scroll_to_selected()
|
||||||
|
if self.selected == 0 then return end
|
||||||
|
local h = self:get_line_height()
|
||||||
|
local y = h * (self.selected - 1)
|
||||||
|
self.scroll.to.y = math.min(self.scroll.to.y, y)
|
||||||
|
self.scroll.to.y = math.max(self.scroll.to.y, y + h - self.size.y)
|
||||||
|
end
|
||||||
|
|
||||||
|
function SearchReplaceList:on_mouse_moved(mx, my, ...)
|
||||||
|
if not SearchReplaceList.super.on_mouse_moved(self, mx, my, ...) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
self.hovered = 0
|
||||||
|
for i, item, x,y,w,h in self:each_visible_item() do
|
||||||
|
w = self.size.x
|
||||||
|
if mx >= x and my >= y and mx < x + w and my < y + h then
|
||||||
|
self.hovered = i
|
||||||
|
x = x + style.padding.x / 2
|
||||||
|
w = style.icon_font:get_width('-')
|
||||||
|
if item.file and mx >= x and my >= y and mx < x + w and my < y + h then
|
||||||
|
self.hovered_expander = true
|
||||||
|
self.hovered_checkbox = false
|
||||||
|
else
|
||||||
|
self.hovered_expander = false
|
||||||
|
end
|
||||||
|
if not self.hovered_expander and self.replacement then
|
||||||
|
x = x + w + style.padding.x / 2
|
||||||
|
w, h, y = self:get_checkbox_size(y)
|
||||||
|
if mx >= x and my >= y and mx < x + w and my < y + h then
|
||||||
|
self.hovered_checkbox = true
|
||||||
|
self.hovered_expander = false
|
||||||
|
else
|
||||||
|
self.hovered_checkbox = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function SearchReplaceList:on_mouse_pressed(button, x, y, clicks)
|
||||||
|
if
|
||||||
|
not SearchReplaceList.super.on_mouse_pressed(self, button, x, y, clicks)
|
||||||
|
then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if self:scrollbar_hovering() then return true end
|
||||||
|
local item = self.items[self.hovered]
|
||||||
|
if not item then return true end
|
||||||
|
self.selected = self.hovered
|
||||||
|
if self.hovered_checkbox then
|
||||||
|
self:toggle_check(self.selected)
|
||||||
|
elseif item.file and (clicks > 1 or self.hovered_expander) then
|
||||||
|
if not item.file.expanded then
|
||||||
|
self:expand(self.hovered)
|
||||||
|
else
|
||||||
|
self:contract(self.hovered)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self:on_item_click(item, clicks)
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function SearchReplaceList:draw_checkbox(checked, hovered, x, y, lh)
|
||||||
|
local w, h, cy = self:get_checkbox_size(y)
|
||||||
|
renderer.draw_rect(x, cy, w, h, style.text)
|
||||||
|
renderer.draw_rect(
|
||||||
|
x + 2, cy + 2, w-4, h-4,
|
||||||
|
hovered and style.dim or style.background
|
||||||
|
)
|
||||||
|
if checked then
|
||||||
|
renderer.draw_rect(x + 5, cy + 5, w-10, h-10, style.caret)
|
||||||
|
end
|
||||||
|
return x + w
|
||||||
|
end
|
||||||
|
|
||||||
|
function SearchReplaceList:draw()
|
||||||
|
if not SearchReplaceList.super.draw(self) then return false end
|
||||||
|
|
||||||
|
core.push_clip_rect(
|
||||||
|
self.position.x,
|
||||||
|
self.position.y,
|
||||||
|
self.size.x,
|
||||||
|
self.size.y
|
||||||
|
)
|
||||||
|
|
||||||
|
local font = self:get_font()
|
||||||
|
|
||||||
|
local replacement = self.replacement
|
||||||
|
local replacement_width = font:get_width(replacement or "")
|
||||||
|
local file_path
|
||||||
|
|
||||||
|
self.max_width = 0
|
||||||
|
|
||||||
|
for i, item, x,y,w,h in self:each_visible_item() do
|
||||||
|
if item.file then
|
||||||
|
file_path = common.relative_path(self.base_dir, item.file.path)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- add left padding
|
||||||
|
x = x + style.padding.x / 2
|
||||||
|
|
||||||
|
local text_color = style.text
|
||||||
|
|
||||||
|
if i == self.selected then
|
||||||
|
renderer.draw_rect(self.position.x, y, self.size.x, h, style.selection)
|
||||||
|
elseif i == self.hovered then
|
||||||
|
renderer.draw_rect(self.position.x, y, self.size.x, h, style.line_highlight)
|
||||||
|
text_color = style.accent
|
||||||
|
end
|
||||||
|
|
||||||
|
-- draw collapse/contract symbol
|
||||||
|
if item.file then
|
||||||
|
common.draw_text(
|
||||||
|
style.icon_font,
|
||||||
|
(self.hovered == i and self.hovered_expander) and style.accent or style.text,
|
||||||
|
item.file.expanded and "-" or "+",
|
||||||
|
"left",
|
||||||
|
x, y, w, h
|
||||||
|
)
|
||||||
|
x = x + style.icon_font:get_width("-")
|
||||||
|
else
|
||||||
|
x = x + style.icon_font:get_width("-")
|
||||||
|
end
|
||||||
|
|
||||||
|
x = x + style.padding.x / 2
|
||||||
|
|
||||||
|
-- draw checkbox
|
||||||
|
if replacement then
|
||||||
|
local checked = false
|
||||||
|
if item.file then
|
||||||
|
checked = item.checked
|
||||||
|
else
|
||||||
|
if type(item.position.checked) == "nil" then
|
||||||
|
checked = true
|
||||||
|
else
|
||||||
|
checked = item.position.checked
|
||||||
|
end
|
||||||
|
end
|
||||||
|
x = style.padding.x / 2 + self:draw_checkbox(
|
||||||
|
checked,
|
||||||
|
i == self.hovered and self.hovered_checkbox or false,
|
||||||
|
x, y, h
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- draw text
|
||||||
|
local text = item.file and file_path or item.line.text
|
||||||
|
local all_text = ""
|
||||||
|
|
||||||
|
if item.line then
|
||||||
|
local start_pos, end_pos = 1, #text
|
||||||
|
local prefix, postfix = "", ""
|
||||||
|
-- truncate long lines to keep good rendering performance
|
||||||
|
if #text > 120 then
|
||||||
|
start_pos = math.max(item.position.col1 - 50, 1)
|
||||||
|
end_pos = math.min(item.position.col2 + 50, end_pos)
|
||||||
|
if start_pos ~= 1 then prefix = "..." end
|
||||||
|
if end_pos ~= #text then postfix = "..." end
|
||||||
|
end
|
||||||
|
x = common.draw_text(
|
||||||
|
font,
|
||||||
|
style.syntax["number"],
|
||||||
|
tostring(item.line.line) .. ": ",
|
||||||
|
"left",
|
||||||
|
x, y, w, h
|
||||||
|
)
|
||||||
|
local start_text = item.position.col1 ~= 1 and
|
||||||
|
prefix .. text:sub(start_pos, item.position.col1-1)
|
||||||
|
or
|
||||||
|
""
|
||||||
|
local end_text = item.position.col2 ~= #text and
|
||||||
|
text:sub(item.position.col2+1, end_pos) .. postfix
|
||||||
|
or
|
||||||
|
""
|
||||||
|
local found_text = text:sub(item.position.col1, item.position.col2)
|
||||||
|
local found_width = style.font:get_width(found_text)
|
||||||
|
if start_text ~= "" then
|
||||||
|
x = common.draw_text(
|
||||||
|
font,
|
||||||
|
text_color,
|
||||||
|
start_text,
|
||||||
|
"left",
|
||||||
|
x, y, w, h
|
||||||
|
)
|
||||||
|
end
|
||||||
|
local found_color = not replacement and style.dim or DIFF.DEL
|
||||||
|
local found_text_color = not replacement and text_color or DIFF.TEXT
|
||||||
|
renderer.draw_rect(x, y, found_width, h, found_color)
|
||||||
|
x = common.draw_text(font, found_text_color, found_text, "left", x, y, w, h)
|
||||||
|
if replacement then
|
||||||
|
renderer.draw_rect(x, y, replacement_width, h, DIFF.ADD)
|
||||||
|
x = common.draw_text(font, DIFF.TEXT, replacement, "left", x, y, w, h)
|
||||||
|
end
|
||||||
|
if end_text ~= "" then
|
||||||
|
x = common.draw_text(
|
||||||
|
font,
|
||||||
|
text_color,
|
||||||
|
end_text,
|
||||||
|
"left",
|
||||||
|
x, y, w, h
|
||||||
|
)
|
||||||
|
end
|
||||||
|
all_text = item.line.line .. ": " .. start_text .. found_text .. end_text
|
||||||
|
else
|
||||||
|
x = common.draw_text(font, text_color, text, "left", x, y, w, h)
|
||||||
|
all_text = file_path
|
||||||
|
end
|
||||||
|
|
||||||
|
-- recalc max_width for horizontal scrollbar
|
||||||
|
self.max_width = math.max(self.max_width, style.font:get_width(all_text))
|
||||||
|
end
|
||||||
|
|
||||||
|
core.pop_clip_rect()
|
||||||
|
|
||||||
|
self:draw_scrollbar()
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return SearchReplaceList
|
||||||
+288
@@ -0,0 +1,288 @@
|
|||||||
|
--
|
||||||
|
-- SelectBox Widget.
|
||||||
|
-- @copyright Jefferson Gonzalez
|
||||||
|
-- @license MIT
|
||||||
|
--
|
||||||
|
|
||||||
|
local core = require "core"
|
||||||
|
local common = require "core.common"
|
||||||
|
local style = require "core.style"
|
||||||
|
local Widget = require "libraries.widget"
|
||||||
|
local ListBox = require "libraries.widget.listbox"
|
||||||
|
|
||||||
|
---@class widget.selectbox : widget
|
||||||
|
---@overload fun(parent?:widget, label?:string):widget.selectbox
|
||||||
|
---@field private list_container widget
|
||||||
|
---@field private list widget.listbox
|
||||||
|
---@field private selected integer
|
||||||
|
---@field private hover_text renderer.color
|
||||||
|
local SelectBox = Widget:extend()
|
||||||
|
|
||||||
|
---Constructor
|
||||||
|
---@param parent widget
|
||||||
|
---@param label string
|
||||||
|
function SelectBox:new(parent, label)
|
||||||
|
SelectBox.super.new(self, parent)
|
||||||
|
self.type_name = "widget.selectbox"
|
||||||
|
self.size.x = 200 + (style.padding.x * 2)
|
||||||
|
self.size.y = self:get_font():get_height() + (style.padding.y * 2)
|
||||||
|
self.list_container = Widget()
|
||||||
|
self.list_container.name = self:get_name()
|
||||||
|
self.list_container:set_size(
|
||||||
|
self.size.x - self.list_container.border.width,
|
||||||
|
150
|
||||||
|
)
|
||||||
|
self.list = ListBox(self.list_container)
|
||||||
|
self.list.border.width = 0
|
||||||
|
self.list:enable_expand(true)
|
||||||
|
self.selected = 0
|
||||||
|
|
||||||
|
local list_on_row_click = self.list.on_row_click
|
||||||
|
self.list.on_row_click = function(this, idx, data)
|
||||||
|
list_on_row_click(this, idx, data)
|
||||||
|
if idx ~= 1 then
|
||||||
|
self.selected = idx-1
|
||||||
|
self:on_selected(idx-1, data)
|
||||||
|
self:on_change(self.selected)
|
||||||
|
end
|
||||||
|
self.list_container:hide_animated(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Hide list if mouse clicked outside
|
||||||
|
self.list_container:force_event("mouse_released")
|
||||||
|
self.list_container.on_mouse_released = function(this, button, x, y)
|
||||||
|
if
|
||||||
|
this:is_visible()
|
||||||
|
and
|
||||||
|
not this:mouse_on_top(x, y) and not self:mouse_on_top(x, y)
|
||||||
|
then
|
||||||
|
this:hide()
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if this:is_visible() and this:mouse_on_top(x, y) then
|
||||||
|
return Widget.on_mouse_released(this, button, x, y)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self:set_label(label or "select")
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set the text displayed when no item is selected.
|
||||||
|
---@param text string
|
||||||
|
function SelectBox:set_label(text)
|
||||||
|
SelectBox.super.set_label(self, "- "..text.." -")
|
||||||
|
if not self.list.rows[1] then
|
||||||
|
self.list:add_row({"- "..text.." -"})
|
||||||
|
else
|
||||||
|
self.list.rows[1][1] = "- "..text.." -"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Add selectable option to the selectbox.
|
||||||
|
---@param text widget.styledtext|string
|
||||||
|
---@param data any
|
||||||
|
function SelectBox:add_option(text, data)
|
||||||
|
if type(text) == "string" then
|
||||||
|
self.list:add_row({ text }, data)
|
||||||
|
else
|
||||||
|
self.list:add_row(text, data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Check if a text is longer than the given maximum width and if larger returns
|
||||||
|
---a chopped version with the overflow_chars appended to it.
|
||||||
|
---@param text string
|
||||||
|
---@param max_width number
|
||||||
|
---@param font widget.font Default is style.font
|
||||||
|
---@param overflow_chars? string Default is '...'
|
||||||
|
---@return string chopped_text
|
||||||
|
---@return boolean overflows True if the text overflows
|
||||||
|
function SelectBox:text_overflow(text, max_width, font, overflow_chars)
|
||||||
|
font = self:get_font(font)
|
||||||
|
overflow_chars = overflow_chars or "..."
|
||||||
|
|
||||||
|
local overflow = false
|
||||||
|
local overflow_chars_width = font:get_width(overflow_chars)
|
||||||
|
|
||||||
|
if font:get_width(text) > max_width then
|
||||||
|
overflow = true
|
||||||
|
for i = 1, #text do
|
||||||
|
local reduced_text = text:sub(1, #text - i)
|
||||||
|
if
|
||||||
|
font:get_width(reduced_text) + overflow_chars_width
|
||||||
|
<=
|
||||||
|
max_width
|
||||||
|
then
|
||||||
|
text = reduced_text .. "…"
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return text, overflow
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set the active option index.
|
||||||
|
---@param idx integer
|
||||||
|
function SelectBox:set_selected(idx)
|
||||||
|
if self.list.rows[idx+1] then
|
||||||
|
self.selected = idx
|
||||||
|
self.list:set_selected(idx+1)
|
||||||
|
else
|
||||||
|
self.selected = 0
|
||||||
|
self.list:set_selected()
|
||||||
|
end
|
||||||
|
self:on_change(self.selected)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the currently selected option index.
|
||||||
|
---@return integer
|
||||||
|
function SelectBox:get_selected()
|
||||||
|
return self.selected
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the currently selected option text.
|
||||||
|
---@return string|nil
|
||||||
|
function SelectBox:get_selected_text()
|
||||||
|
if self.selected > 0 then
|
||||||
|
return self.list:get_row_text(self.selected + 1)
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the currently selected option associated data.
|
||||||
|
---@return any|nil
|
||||||
|
function SelectBox:get_selected_data()
|
||||||
|
if self.selected > 0 then
|
||||||
|
return self.list:get_row_data(self.selected + 1)
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---Repositions the listbox container according to the selectbox position
|
||||||
|
---and available screensize.
|
||||||
|
function SelectBox:reposition_container()
|
||||||
|
local y1 = self.position.y + self:get_height()
|
||||||
|
local y2 = self.position.y - self.list:get_height()
|
||||||
|
|
||||||
|
local _, h = system.get_window_size(core.window)
|
||||||
|
|
||||||
|
if y1 + self.list:get_height() <= h then
|
||||||
|
self.list_container:set_position(
|
||||||
|
self.position.x,
|
||||||
|
y1
|
||||||
|
)
|
||||||
|
else
|
||||||
|
self.list_container:set_position(
|
||||||
|
self.position.x,
|
||||||
|
y2
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
self.list_container.size.x = self.size.x - (self.border.width * 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Overrided to destroy the floating listbox container.
|
||||||
|
function SelectBox:destroy_childs()
|
||||||
|
SelectBox.super.destroy_childs(self)
|
||||||
|
self.list_container:destroy()
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Events
|
||||||
|
--
|
||||||
|
|
||||||
|
---Overwrite to listen to on_selected events.
|
||||||
|
---@param item_idx integer
|
||||||
|
---@param item_data widget.listbox.row
|
||||||
|
---@diagnostic disable-next-line
|
||||||
|
function SelectBox:on_selected(item_idx, item_data) end
|
||||||
|
|
||||||
|
function SelectBox:on_mouse_enter(...)
|
||||||
|
SelectBox.super.on_mouse_enter(self, ...)
|
||||||
|
self.hover_text = style.accent
|
||||||
|
end
|
||||||
|
|
||||||
|
function SelectBox:on_mouse_leave(...)
|
||||||
|
SelectBox.super.on_mouse_leave(self, ...)
|
||||||
|
self.hover_text = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function SelectBox:on_click(button, x, y)
|
||||||
|
SelectBox.super.on_click(self, button, x, y)
|
||||||
|
|
||||||
|
if button == "left" then
|
||||||
|
self:reposition_container()
|
||||||
|
|
||||||
|
self.list_container.border.color = style.caret
|
||||||
|
|
||||||
|
self.list_container:toggle_visible(true, true)
|
||||||
|
|
||||||
|
self.list:resize_to_parent()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function SelectBox:update()
|
||||||
|
if not SelectBox.super.update(self) then return false end
|
||||||
|
|
||||||
|
local font = self:get_font()
|
||||||
|
|
||||||
|
self.size.y = font:get_height() + style.padding.y * 2
|
||||||
|
|
||||||
|
if
|
||||||
|
self.list_container.visible
|
||||||
|
and
|
||||||
|
self.list_container.position.y ~= self.position.y + self:get_height()
|
||||||
|
then
|
||||||
|
self:reposition_container()
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function SelectBox:draw()
|
||||||
|
if not SelectBox.super.draw(self) then return false end
|
||||||
|
|
||||||
|
local font = self:get_font()
|
||||||
|
|
||||||
|
local icon_width = style.icon_font:get_width("+")
|
||||||
|
|
||||||
|
local max_width = self.size.x
|
||||||
|
- icon_width
|
||||||
|
- (style.padding.x * 2) -- left/right paddings
|
||||||
|
- (style.padding.x / 2) -- space between icon and text
|
||||||
|
|
||||||
|
local item_text = self.selected == 0 and
|
||||||
|
self.label or self.list:get_row_text(self.selected+1)
|
||||||
|
|
||||||
|
local text = self:text_overflow(item_text, max_width, font)
|
||||||
|
|
||||||
|
-- draw label or selected item
|
||||||
|
common.draw_text(
|
||||||
|
font,
|
||||||
|
self.hover_text or self.foreground_color or style.text,
|
||||||
|
text,
|
||||||
|
"left",
|
||||||
|
self.position.x + style.padding.x,
|
||||||
|
self.position.y,
|
||||||
|
self.size.x - style.padding.x,
|
||||||
|
self.size.y
|
||||||
|
)
|
||||||
|
|
||||||
|
-- draw arrow down icon
|
||||||
|
common.draw_text(
|
||||||
|
style.icon_font,
|
||||||
|
self.hover_text or self.foreground_color or style.text,
|
||||||
|
"-",
|
||||||
|
"right",
|
||||||
|
self.position.x,
|
||||||
|
self.position.y,
|
||||||
|
self.size.x - style.padding.x,
|
||||||
|
self.size.y
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return SelectBox
|
||||||
+308
@@ -0,0 +1,308 @@
|
|||||||
|
--
|
||||||
|
-- TextBox widget re-using code from lite's DocView.
|
||||||
|
--
|
||||||
|
|
||||||
|
local core = require "core"
|
||||||
|
local style = require "core.style"
|
||||||
|
local translate = require "core.doc.translate"
|
||||||
|
local Doc = require "core.doc"
|
||||||
|
local DocView = require "core.docview"
|
||||||
|
local View = require "core.view"
|
||||||
|
local Widget = require "libraries.widget"
|
||||||
|
|
||||||
|
|
||||||
|
---@class widget.textbox.SingleLineDoc : core.doc
|
||||||
|
---@overload fun():widget.textbox.SingleLineDoc
|
||||||
|
---@field super core.doc
|
||||||
|
local SingleLineDoc = Doc:extend()
|
||||||
|
|
||||||
|
function SingleLineDoc:insert(line, col, text)
|
||||||
|
SingleLineDoc.super.insert(self, line, col, text:gsub("\n", ""))
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class widget.textbox.TextView : core.docview
|
||||||
|
---@overload fun():widget.textbox.TextView
|
||||||
|
---@field super core.docview
|
||||||
|
local TextView = DocView:extend()
|
||||||
|
|
||||||
|
function TextView:new()
|
||||||
|
TextView.super.new(self, SingleLineDoc())
|
||||||
|
self.gutter_width = 0
|
||||||
|
self.hide_lines_gutter = true
|
||||||
|
self.gutter_text_brightness = 0
|
||||||
|
self.scrollable = true
|
||||||
|
self.font = "font"
|
||||||
|
self.name = View.get_name(self)
|
||||||
|
|
||||||
|
self.size.y = 0
|
||||||
|
self.label = ""
|
||||||
|
end
|
||||||
|
|
||||||
|
function TextView:get_name()
|
||||||
|
return self.name
|
||||||
|
end
|
||||||
|
|
||||||
|
function TextView:get_scrollable_size()
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function TextView:get_text()
|
||||||
|
return self.doc:get_text(1, 1, 1, math.huge)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TextView:set_text(text, select)
|
||||||
|
self.doc:remove(1, 1, math.huge, math.huge)
|
||||||
|
self.doc:text_input(text)
|
||||||
|
if select then
|
||||||
|
self.doc:set_selection(math.huge, math.huge, 1, 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function TextView:get_gutter_width()
|
||||||
|
return self.gutter_width or 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function TextView:get_line_height()
|
||||||
|
return math.floor(self:get_font():get_height() * 1.2)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TextView:draw_line_gutter(idx, x, y)
|
||||||
|
if self.hide_lines_gutter then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
TextView.super.draw_line_gutter(self, idx, x, y)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TextView:draw_line_highlight()
|
||||||
|
-- no-op function to disable this functionality
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Overwrite this function just to disable the core.push_clip_rect
|
||||||
|
function TextView:draw()
|
||||||
|
self:draw_background(style.background)
|
||||||
|
local _, indent_size = self.doc:get_indent_info()
|
||||||
|
self:get_font():set_tab_size(indent_size)
|
||||||
|
|
||||||
|
local minline, maxline = self:get_visible_line_range()
|
||||||
|
local lh = self:get_line_height()
|
||||||
|
|
||||||
|
local x, y = self:get_line_screen_position(minline)
|
||||||
|
for i = minline, maxline do
|
||||||
|
self:draw_line_gutter(i, self.position.x, y)
|
||||||
|
y = y + lh
|
||||||
|
end
|
||||||
|
|
||||||
|
x, y = self:get_line_screen_position(minline)
|
||||||
|
for i = minline, maxline do
|
||||||
|
self:draw_line_body(i, x, y)
|
||||||
|
y = y + lh
|
||||||
|
end
|
||||||
|
self:draw_overlay()
|
||||||
|
|
||||||
|
self:draw_scrollbar()
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class widget.textbox : widget
|
||||||
|
---@overload fun(parent?:widget, text?:string, placeholder?:string):widget.textbox
|
||||||
|
---@field textview widget.textbox.TextView
|
||||||
|
---@field placeholder string
|
||||||
|
---@field placeholder_active boolean
|
||||||
|
local TextBox = Widget:extend()
|
||||||
|
|
||||||
|
function TextBox:new(parent, text, placeholder)
|
||||||
|
TextBox.super.new(self, parent)
|
||||||
|
self.type_name = "widget.textbox"
|
||||||
|
self.textview = TextView()
|
||||||
|
self.textview.name = parent.name
|
||||||
|
self.size.x = 200 + (style.padding.x * 2)
|
||||||
|
self.textview.size.x = self.size.x
|
||||||
|
self.size.y = self:get_font():get_height() + (style.padding.y * 2)
|
||||||
|
self.placeholder = placeholder or ""
|
||||||
|
self.placeholder_active = false
|
||||||
|
-- this widget is for text input
|
||||||
|
self.input_text = true
|
||||||
|
self.cursor = "ibeam"
|
||||||
|
self.active = false
|
||||||
|
self.drag_select = false
|
||||||
|
|
||||||
|
if text ~= "" then
|
||||||
|
self.textview:set_text(text, select)
|
||||||
|
else
|
||||||
|
self.placeholder_active = true
|
||||||
|
self.textview:set_text(self.placeholder)
|
||||||
|
end
|
||||||
|
|
||||||
|
local this = self
|
||||||
|
|
||||||
|
function self.textview.doc:on_text_change()
|
||||||
|
if not this.placeholder_active then
|
||||||
|
this:on_change(this:get_text())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- more granular listening of text changing events
|
||||||
|
local doc_raw_insert = self.textview.doc.raw_insert
|
||||||
|
function self.textview.doc:raw_insert(...)
|
||||||
|
doc_raw_insert(self, ...)
|
||||||
|
this:on_text_change("insert", ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
local doc_raw_remove = self.textview.doc.raw_remove
|
||||||
|
function self.textview.doc:raw_remove(...)
|
||||||
|
doc_raw_remove(self, ...)
|
||||||
|
this:on_text_change("remove", ...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param width integer
|
||||||
|
function TextBox:set_size(width)
|
||||||
|
TextBox.super.set_size(
|
||||||
|
self,
|
||||||
|
width,
|
||||||
|
self:get_font():get_height() + (style.padding.y * 2)
|
||||||
|
)
|
||||||
|
self.textview.size.x = self.size.x
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get the text displayed on the textbox.
|
||||||
|
---@return string
|
||||||
|
function TextBox:get_text()
|
||||||
|
if self.placeholder_active then
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
return self.textview:get_text()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Set the text displayed on the textbox.
|
||||||
|
---@param text string
|
||||||
|
---@param select? boolean
|
||||||
|
function TextBox:set_text(text, select)
|
||||||
|
self.textview:set_text(text, select)
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Events
|
||||||
|
--
|
||||||
|
|
||||||
|
function TextBox:on_mouse_pressed(button, x, y, clicks)
|
||||||
|
if TextBox.super.on_mouse_pressed(self, button, x, y, clicks) then
|
||||||
|
self.textview:on_mouse_pressed(button, x, y, clicks)
|
||||||
|
local line, col = self.textview:resolve_screen_position(x, y)
|
||||||
|
self.drag_select = { line = line, col = col }
|
||||||
|
self.textview.doc:set_selection(line, col, line, col)
|
||||||
|
if clicks == 2 then
|
||||||
|
local line1, col1 = translate.start_of_word(self.textview.doc, line, col)
|
||||||
|
local line2, col2 = translate.end_of_word(self.textview.doc, line1, col1)
|
||||||
|
self.textview.doc:set_selection(line2, col2, line1, col1)
|
||||||
|
elseif clicks == 3 then
|
||||||
|
self.textview.doc:set_selection(1, 1, 1, math.huge)
|
||||||
|
end
|
||||||
|
if core.active_view ~= self.textview then
|
||||||
|
self.textview:on_mouse_released(button, x, y)
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function TextBox:on_mouse_released(button, x, y)
|
||||||
|
if TextBox.super.on_mouse_released(self, button, x, y) then
|
||||||
|
self.drag_select = false
|
||||||
|
self.textview:on_mouse_released(button, x, y)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function TextBox:on_mouse_moved(x, y, dx, dy)
|
||||||
|
if self.drag_select then
|
||||||
|
local line, col = self.textview:resolve_screen_position(x, y)
|
||||||
|
self.textview.doc:set_selection(
|
||||||
|
self.drag_select.line, self.drag_select.col, line, col
|
||||||
|
)
|
||||||
|
end
|
||||||
|
if TextBox.super.on_mouse_moved(self, x, y, dx, dy) then
|
||||||
|
if self.active or core.active_view == self.textview then
|
||||||
|
self.textview:on_mouse_moved(x, y, dx, dy)
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function TextBox:activate()
|
||||||
|
self.hover_border = style.caret
|
||||||
|
if self.placeholder_active then
|
||||||
|
self.placeholder_active = false
|
||||||
|
self:set_text("")
|
||||||
|
end
|
||||||
|
self.active = true
|
||||||
|
core.request_cursor("ibeam")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TextBox:deactivate()
|
||||||
|
self.hover_border = nil
|
||||||
|
self.drag_select = false
|
||||||
|
if self:get_text() == "" then
|
||||||
|
self.placeholder_active = true
|
||||||
|
self:set_text(self.placeholder)
|
||||||
|
end
|
||||||
|
self.active = false
|
||||||
|
core.request_cursor("arrow")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TextBox:on_text_input(text)
|
||||||
|
TextBox.super.on_text_input(self, text)
|
||||||
|
self.textview:on_text_input(text)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Event fired on any text change event.
|
||||||
|
---@param action string Can be "insert" or "remove",
|
||||||
|
---insert arguments (see Doc:raw_insert):
|
||||||
|
--- line, col, text, undo_stack, time
|
||||||
|
---remove arguments (see Doc:raw_remove):
|
||||||
|
--- line1, col1, line2, col2, undo_stack, time
|
||||||
|
---@diagnostic disable-next-line
|
||||||
|
function TextBox:on_text_change(action, ...) end
|
||||||
|
|
||||||
|
function TextBox:update()
|
||||||
|
if not TextBox.super.update(self) then return false end
|
||||||
|
|
||||||
|
if
|
||||||
|
self.drag_select
|
||||||
|
or
|
||||||
|
(self.active and self:mouse_on_top(self.mouse.x, self.mouse.y))
|
||||||
|
then
|
||||||
|
core.request_cursor("ibeam")
|
||||||
|
end
|
||||||
|
|
||||||
|
self.textview:update()
|
||||||
|
self.size.y = self:get_font():get_height() + (style.padding.y * 2)
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function TextBox:draw()
|
||||||
|
if not self:is_visible() then return false end
|
||||||
|
|
||||||
|
self.border.color = self.hover_border or style.text
|
||||||
|
TextBox.super.draw(self)
|
||||||
|
self.textview.position.x = self.position.x + (style.padding.x / 2)
|
||||||
|
self.textview.position.y = self.position.y - (style.padding.y/2.5)
|
||||||
|
self.textview.size.x = self.size.x
|
||||||
|
self.textview.size.y = self.size.y - (style.padding.y * 2)
|
||||||
|
|
||||||
|
core.push_clip_rect(
|
||||||
|
self.position.x,
|
||||||
|
self.position.y,
|
||||||
|
self.size.x,
|
||||||
|
self.size.y
|
||||||
|
)
|
||||||
|
self.textview:draw()
|
||||||
|
core.pop_clip_rect()
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return TextBox
|
||||||
+140
@@ -0,0 +1,140 @@
|
|||||||
|
--
|
||||||
|
-- Toggle Widget.
|
||||||
|
-- @copyright Jefferson Gonzalez
|
||||||
|
-- @license MIT
|
||||||
|
--
|
||||||
|
|
||||||
|
local style = require "core.style"
|
||||||
|
local Widget = require "libraries.widget"
|
||||||
|
local Label = require "libraries.widget.label"
|
||||||
|
|
||||||
|
-- Hold dimensions of rendered toggle
|
||||||
|
local BOX = 40
|
||||||
|
local TOGGLE = 15
|
||||||
|
local BORDER = 3
|
||||||
|
|
||||||
|
---@class widget.toggle : widget
|
||||||
|
---@overload fun(parent?:widget, label?:string, enable?:boolean):widget.toggle
|
||||||
|
---@field public enabled boolean
|
||||||
|
---@field private caption_label widget.textbox
|
||||||
|
---@field private padding integer
|
||||||
|
---@field private switch_x number
|
||||||
|
---@field private toggle_bg renderer.color
|
||||||
|
local Toggle = Widget:extend()
|
||||||
|
|
||||||
|
---Constructor
|
||||||
|
---@param parent widget
|
||||||
|
---@param label string
|
||||||
|
---@param enable boolean
|
||||||
|
function Toggle:new(parent, label, enable)
|
||||||
|
Toggle.super.new(self, parent)
|
||||||
|
|
||||||
|
self.type_name = "widget.toggle"
|
||||||
|
|
||||||
|
self.enabled = enable or false
|
||||||
|
self.label = label or ""
|
||||||
|
|
||||||
|
self.caption_label = Label(self, self.label)
|
||||||
|
self.caption_label:set_position(0, 0)
|
||||||
|
|
||||||
|
self.padding = 2
|
||||||
|
self.border.width = 0
|
||||||
|
|
||||||
|
self:set_size(
|
||||||
|
self.caption_label:get_width() + (style.padding.x / 2) + (BOX * SCALE),
|
||||||
|
self.caption_label:get_height() + ((self.padding * 2) * SCALE)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.animate_switch = false
|
||||||
|
self.toggle_x = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param enabled boolean
|
||||||
|
function Toggle:set_toggle(enabled)
|
||||||
|
self.enabled = enabled
|
||||||
|
self.animate_switch = true
|
||||||
|
self:on_change(self.enabled)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return boolean
|
||||||
|
function Toggle:is_toggled()
|
||||||
|
return self.enabled
|
||||||
|
end
|
||||||
|
|
||||||
|
function Toggle:toggle()
|
||||||
|
self.enabled = not self.enabled
|
||||||
|
self.animate_switch = true
|
||||||
|
self:on_change(self.enabled)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param text string|widget.styledtext
|
||||||
|
function Toggle:set_label(text)
|
||||||
|
Toggle.super.set_label(self, text)
|
||||||
|
self.caption_label:set_label(text)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Toggle:on_click()
|
||||||
|
self:toggle()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Toggle:update()
|
||||||
|
if not Toggle.super.update(self) then return false end
|
||||||
|
|
||||||
|
local px = style.padding.x / 2
|
||||||
|
|
||||||
|
self:set_size(
|
||||||
|
self.caption_label:get_width() + px + (BOX * SCALE),
|
||||||
|
self.caption_label:get_height() + ((self.padding * 2) * SCALE)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.toggle_x = self.caption_label:get_right() + px
|
||||||
|
|
||||||
|
local switch_x = self.enabled and
|
||||||
|
self.position.x + self.toggle_x
|
||||||
|
+ ((BOX - TOGGLE - BORDER) * SCALE)
|
||||||
|
or
|
||||||
|
self.position.x + self.toggle_x + (BORDER * SCALE)
|
||||||
|
|
||||||
|
if not self.animate_switch then
|
||||||
|
self.switch_x = switch_x
|
||||||
|
self.toggle_bg = {}
|
||||||
|
local color = self.enabled and style.caret or style.line_number
|
||||||
|
for i=1, 4, 1 do self.toggle_bg[i] = color[i] end
|
||||||
|
else
|
||||||
|
local color = self.enabled and style.caret or style.line_number
|
||||||
|
self:move_towards(self, "switch_x", switch_x, 0.2)
|
||||||
|
for i=1, 4, 1 do
|
||||||
|
self:move_towards(self.toggle_bg, i, color[i], 0.2)
|
||||||
|
end
|
||||||
|
if self.switch_x == switch_x then
|
||||||
|
self.animate_switch = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function Toggle:draw()
|
||||||
|
if not Toggle.super.draw(self) then return false end
|
||||||
|
|
||||||
|
renderer.draw_rect(
|
||||||
|
self.position.x + self.toggle_x,
|
||||||
|
self.position.y,
|
||||||
|
BOX * SCALE,
|
||||||
|
self.size.y,
|
||||||
|
self.toggle_bg
|
||||||
|
)
|
||||||
|
|
||||||
|
renderer.draw_rect(
|
||||||
|
self.switch_x,
|
||||||
|
self.position.y + (BORDER * SCALE),
|
||||||
|
TOGGLE * SCALE,
|
||||||
|
self.size.y - ((BORDER * 2) * SCALE),
|
||||||
|
style.line_highlight
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return Toggle
|
||||||
+609
@@ -0,0 +1,609 @@
|
|||||||
|
--
|
||||||
|
-- TreeList Widget heavily based on TreeView plugin.
|
||||||
|
-- @copyright Jefferson Gonzalez
|
||||||
|
-- @license MIT
|
||||||
|
--
|
||||||
|
|
||||||
|
local core = require "core"
|
||||||
|
local common = require "core.common"
|
||||||
|
local style = require "core.style"
|
||||||
|
local Widget = require "libraries.widget"
|
||||||
|
|
||||||
|
---@class widget.treelist.item
|
||||||
|
---@field name string
|
||||||
|
---@field label string
|
||||||
|
---@field data any?
|
||||||
|
---@field expanded boolean?
|
||||||
|
---@field visible boolean?
|
||||||
|
---@field tooltip string?
|
||||||
|
---@field depth integer?
|
||||||
|
---@field childs widget.treelist.item[]?
|
||||||
|
---@field parent widget.treelist.item[]?
|
||||||
|
---@field icon string?
|
||||||
|
|
||||||
|
---@class widget.treelist : widget
|
||||||
|
---@field items widget.treelist.item[]
|
||||||
|
---@field selected_item widget.treelist.item?
|
||||||
|
---@field hovered_item widget.treelist.item?
|
||||||
|
---@field icon_font renderer.font?
|
||||||
|
---@field private count_lines integer
|
||||||
|
---@field private scroll_width integer
|
||||||
|
---@overload fun(parent:widget?):widget.treelist
|
||||||
|
local TreeList = Widget:extend()
|
||||||
|
|
||||||
|
---Constructor
|
||||||
|
---@param parent? widget
|
||||||
|
function TreeList:new(parent)
|
||||||
|
TreeList.super.new(self, parent)
|
||||||
|
|
||||||
|
self.type_name = "widget.treelist"
|
||||||
|
|
||||||
|
self.scrollable = true
|
||||||
|
self.init_size = true
|
||||||
|
self.count_lines = 0
|
||||||
|
self.scroll_width = 0
|
||||||
|
self.items = {}
|
||||||
|
self.tooltip = { x = 0, y = 0, begin = 0, alpha = 0 }
|
||||||
|
self.last_scroll_y = 0
|
||||||
|
|
||||||
|
self.item_icon_width = 0
|
||||||
|
self.item_text_spacing = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the height of a single item.
|
||||||
|
---@return number h
|
||||||
|
function TreeList:get_item_height()
|
||||||
|
return style.font:get_height() + style.padding.y
|
||||||
|
end
|
||||||
|
|
||||||
|
---Add new item to to tree list
|
||||||
|
---@param item widget.treelist.item
|
||||||
|
function TreeList:add_item(item)
|
||||||
|
table.insert(self.items, item)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Remove all items from the tree
|
||||||
|
function TreeList:clear()
|
||||||
|
self.items = {}
|
||||||
|
self.selected_item = nil
|
||||||
|
self.hovered_item = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---Retrieve the amount of visible items and also yield them.
|
||||||
|
---@param item widget.treelist.item
|
||||||
|
---@param x number
|
||||||
|
---@param y number
|
||||||
|
---@param w number
|
||||||
|
---@param h number
|
||||||
|
---@param depth? number
|
||||||
|
---@return integer items_count
|
||||||
|
function TreeList:get_items(item, x, y, w, h, depth)
|
||||||
|
if not depth then depth = 0 else depth = depth + 1 end
|
||||||
|
item.depth = depth
|
||||||
|
coroutine.yield(item, x, y, w, h)
|
||||||
|
local count_lines = 1
|
||||||
|
if item and item.childs and item.expanded then
|
||||||
|
for _, child in ipairs(item.childs) do
|
||||||
|
if child.visible ~= false then
|
||||||
|
count_lines = count_lines + self:get_items(
|
||||||
|
child, x, y + count_lines * h, w, h, depth
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return count_lines
|
||||||
|
end
|
||||||
|
|
||||||
|
---Allows iterating the currently visible items only.
|
||||||
|
---@return fun():widget.treelist.item,number,number,number,number
|
||||||
|
function TreeList:each_item()
|
||||||
|
return coroutine.wrap(function()
|
||||||
|
local count_lines = 0
|
||||||
|
local ox, oy = self:get_content_offset()
|
||||||
|
local h = self:get_item_height()
|
||||||
|
if #self.items > 0 then
|
||||||
|
for i=1, #self.items do
|
||||||
|
count_lines = count_lines + self:get_items(
|
||||||
|
self.items[i],
|
||||||
|
ox, oy + style.padding.y + h * count_lines,
|
||||||
|
self.size.x, h
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.count_lines = count_lines
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Retrieve an item by name using the query format:
|
||||||
|
---"parent_name>child_name_2>child_name_2>etc..."
|
||||||
|
---@param query string
|
||||||
|
---@param items? widget.treelist.item[]
|
||||||
|
---@param separator? string Use a different separator (default: >)
|
||||||
|
---@return widget.treelist.item?
|
||||||
|
function TreeList:query_item(query, items, separator)
|
||||||
|
local parent = items or self.items
|
||||||
|
local item = nil
|
||||||
|
separator = separator or ">"
|
||||||
|
for name in query:gmatch("([^"..separator.."]+)") do
|
||||||
|
if parent then
|
||||||
|
local found = false
|
||||||
|
for _, child in ipairs(parent) do
|
||||||
|
if name == child.name then
|
||||||
|
item = child
|
||||||
|
parent = child.childs
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not found then return nil end
|
||||||
|
else
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return item
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set the active item.
|
||||||
|
---@param selection widget.treelist.item
|
||||||
|
---@param selection_y? number
|
||||||
|
---@param center? boolean
|
||||||
|
---@param instant? boolean
|
||||||
|
function TreeList:set_selection(selection, selection_y, center, instant)
|
||||||
|
self.selected_item = selection
|
||||||
|
if selection and selection_y
|
||||||
|
and (selection_y <= 0 or selection_y >= self.size.y) then
|
||||||
|
local lh = self:get_item_height()
|
||||||
|
if not center and selection_y >= self.size.y - lh then
|
||||||
|
selection_y = selection_y - self.size.y + lh
|
||||||
|
end
|
||||||
|
if center then
|
||||||
|
selection_y = selection_y - (self.size.y - lh) / 2
|
||||||
|
end
|
||||||
|
local _, y = self:get_content_offset()
|
||||||
|
self.scroll.to.y = selection_y - y
|
||||||
|
self.scroll.to.y = common.clamp(
|
||||||
|
self.scroll.to.y, 0, self:get_scrollable_size() - self.size.y
|
||||||
|
)
|
||||||
|
if instant then
|
||||||
|
self.scroll.y = self.scroll.to.y
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Sets the selection to the file with the specified path.
|
||||||
|
---TODO: Not tested, idea is to allow query like selections using item names
|
||||||
|
---by introducing a get_item/set_item methods that accepts a string query,
|
||||||
|
---For the moment we leave this inherited TreeView function here.
|
||||||
|
---@param path string #Absolute path of item to select
|
||||||
|
---@param expand boolean #Expand dirs leading to the item
|
||||||
|
---@param scroll_to boolean #Scroll to make the item visible
|
||||||
|
---@param instant boolean #Don't animate the scroll
|
||||||
|
---@return table? #The selected item
|
||||||
|
function TreeList:set_selection_from_path(path, expand, scroll_to, instant)
|
||||||
|
local separator = "||"
|
||||||
|
local to_select, to_select_y
|
||||||
|
local let_it_finish, done
|
||||||
|
::restart::
|
||||||
|
for item, _,y,_,_ in self:each_item() do
|
||||||
|
if not done then
|
||||||
|
if item.childs and #item.childs > 0 then
|
||||||
|
local _, to = string.find(path, item.name..separator, 1, true)
|
||||||
|
if to and to == #item.name + #separator then
|
||||||
|
to_select, to_select_y = item, y
|
||||||
|
if expand and not item.expanded then
|
||||||
|
self:toggle_expand(true, item)
|
||||||
|
-- Because we altered the size of the TreeList
|
||||||
|
-- and because TreeList:get_scrollable_size uses self.count_lines
|
||||||
|
-- which gets updated only when TreeList:each_item finishes,
|
||||||
|
-- we can't stop here or we risk that the scroll
|
||||||
|
-- gets clamped by View:clamp_scroll_position.
|
||||||
|
let_it_finish = true
|
||||||
|
-- We need to restart the process because if TreeList:toggle_expand
|
||||||
|
-- altered the cache, TreeList:each_item risks looping indefinitely.
|
||||||
|
goto restart
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if item.name == path then
|
||||||
|
to_select, to_select_y = item, y
|
||||||
|
done = true
|
||||||
|
if not let_it_finish then break end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if to_select then
|
||||||
|
self:set_selection(to_select, scroll_to and to_select_y, true, instant)
|
||||||
|
end
|
||||||
|
return to_select
|
||||||
|
end
|
||||||
|
|
||||||
|
---Set the icon font used to render the items icon.
|
||||||
|
---@param font renderer.font
|
||||||
|
function TreeList:set_icon_font(font)
|
||||||
|
if font:get_size() ~= style.icon_font:get_size() then
|
||||||
|
font:set_size(style.icon_font:get_size())
|
||||||
|
end
|
||||||
|
self.icon_font = font
|
||||||
|
end
|
||||||
|
|
||||||
|
---Keep the icon font size updated to match current scale.
|
||||||
|
function TreeList:on_scale_change()
|
||||||
|
if self.icon_font then
|
||||||
|
self.icon_font:set_size(style.icon_font:get_size())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function TreeList:on_mouse_moved(px, py, ...)
|
||||||
|
if TreeList.super.on_mouse_moved(self, px, py, ...) then
|
||||||
|
self.hovered_item = nil
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local position = self:get_position()
|
||||||
|
local item_changed, tooltip_changed
|
||||||
|
for item, x,y,w,h in self:each_item() do
|
||||||
|
if px > x and py > y and px <= (self.size.x + position.x) and py <= y + h then
|
||||||
|
item_changed = true
|
||||||
|
self.hovered_item = item
|
||||||
|
|
||||||
|
x = math.max(x, self.position.x)
|
||||||
|
if px > x and py > y and px <= x + w and py <= y + h then
|
||||||
|
tooltip_changed = true
|
||||||
|
self.tooltip.x, self.tooltip.y = px, py
|
||||||
|
self.tooltip.begin = system.get_time()
|
||||||
|
end
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not item_changed then self.hovered_item = nil end
|
||||||
|
if not tooltip_changed then self.tooltip.x, self.tooltip.y = nil, nil end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
---Override to listen for item click events.
|
||||||
|
---@param item widget.treelist.item
|
||||||
|
---@param button string
|
||||||
|
---@param clicks integer
|
||||||
|
function TreeList:on_item_click(item, button, x, y, clicks) end
|
||||||
|
|
||||||
|
function TreeList:on_mouse_pressed(button, x, y, clicks)
|
||||||
|
if TreeList.super.on_mouse_pressed(self, button, x, y, clicks) then
|
||||||
|
if self:scrollbar_hovering() then return true end
|
||||||
|
self:set_selection(self.hovered_item)
|
||||||
|
if self.hovered_item then
|
||||||
|
local emit_click = false
|
||||||
|
if clicks > 1 then
|
||||||
|
self:toggle_expand()
|
||||||
|
else
|
||||||
|
if self.hovered_item.childs then
|
||||||
|
local x1, x2 = self:get_chevron_position(self.hovered_item)
|
||||||
|
if x >= x1 and x <= x2 then
|
||||||
|
self:toggle_expand()
|
||||||
|
else
|
||||||
|
emit_click = true
|
||||||
|
end
|
||||||
|
else
|
||||||
|
emit_click = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if emit_click then
|
||||||
|
self:on_item_click(self.hovered_item, button, x, y, clicks)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function TreeList:on_mouse_left()
|
||||||
|
TreeList.super.on_mouse_left(self)
|
||||||
|
self.hovered_item = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function TreeList:update()
|
||||||
|
if not self:is_visible() then return end
|
||||||
|
|
||||||
|
local duration = system.get_time() - self.tooltip.begin
|
||||||
|
local tooltip_delay = 0.5
|
||||||
|
if self.hovered_item and self.tooltip.x and duration > tooltip_delay then
|
||||||
|
self:move_towards(self.tooltip, "alpha", 255, 1, "treeview")
|
||||||
|
else
|
||||||
|
self.tooltip.alpha = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
self.item_icon_width = style.icon_font:get_width("D")
|
||||||
|
self.item_text_spacing = style.icon_font:get_width("f") / 2
|
||||||
|
|
||||||
|
-- this will make sure hovered_item is updated
|
||||||
|
local dy = math.abs(self.last_scroll_y - self.scroll.y)
|
||||||
|
if dy > 0 then
|
||||||
|
self:on_mouse_moved(core.root_view.mouse.x, core.root_view.mouse.y, 0, 0)
|
||||||
|
self.last_scroll_y = self.scroll.y
|
||||||
|
end
|
||||||
|
|
||||||
|
TreeList.super.update(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TreeList:get_scrollable_size()
|
||||||
|
return self.count_lines and self:get_item_height() * (self.count_lines + 1) or math.huge
|
||||||
|
end
|
||||||
|
|
||||||
|
function TreeList:get_h_scrollable_size()
|
||||||
|
local _, _, v_scroll_w = self.v_scrollbar:get_thumb_rect()
|
||||||
|
return self.scroll_width + (
|
||||||
|
self.size.x > self.scroll_width + v_scroll_w and 0 or style.padding.x
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function replace_alpha(color, alpha)
|
||||||
|
local r, g, b = table.unpack(color)
|
||||||
|
return { r, g, b, alpha }
|
||||||
|
end
|
||||||
|
|
||||||
|
function TreeList:draw_tooltip()
|
||||||
|
if not self.hovered_item or not self.hovered_item.tooltip then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local text = common.home_encode(self.hovered_item.tooltip)
|
||||||
|
local w, h = style.font:get_width(text), style.font:get_height()
|
||||||
|
|
||||||
|
local tooltip_offset = style.font:get_height()
|
||||||
|
local x, y = self.tooltip.x + tooltip_offset, self.tooltip.y + tooltip_offset
|
||||||
|
w, h = w + style.padding.x, h + style.padding.y
|
||||||
|
|
||||||
|
if x + w > core.root_view.root_node.size.x then -- check if we can span right
|
||||||
|
x = x - w -- span left instead
|
||||||
|
end
|
||||||
|
|
||||||
|
local tooltip_border = 1
|
||||||
|
local bx, by = x - tooltip_border, y - tooltip_border
|
||||||
|
local bw, bh = w + 2 * tooltip_border, h + 2 * tooltip_border
|
||||||
|
renderer.draw_rect(
|
||||||
|
bx, by, bw, bh, replace_alpha(style.text, self.tooltip.alpha)
|
||||||
|
)
|
||||||
|
renderer.draw_rect(
|
||||||
|
x, y, w, h, replace_alpha(style.background2, self.tooltip.alpha)
|
||||||
|
)
|
||||||
|
common.draw_text(
|
||||||
|
style.font,
|
||||||
|
replace_alpha(style.text, self.tooltip.alpha),
|
||||||
|
text, "center", x, y, w, h
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param item widget.treelist.item
|
||||||
|
---@param active boolean
|
||||||
|
---@param hovered boolean
|
||||||
|
function TreeList:get_item_icon(item, active, hovered)
|
||||||
|
local character = item.icon
|
||||||
|
local font = self.icon_font or style.icon_font
|
||||||
|
local color = style.text
|
||||||
|
if active or hovered then
|
||||||
|
color = style.accent
|
||||||
|
end
|
||||||
|
return character, font, color
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param item widget.treelist.item
|
||||||
|
---@param active boolean
|
||||||
|
---@param hovered boolean
|
||||||
|
function TreeList:get_item_text(item, active, hovered)
|
||||||
|
local text = item.label
|
||||||
|
local font = style.font
|
||||||
|
local color = style.text
|
||||||
|
if active or hovered then
|
||||||
|
color = style.accent
|
||||||
|
end
|
||||||
|
return text, font, color
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param item widget.treelist.item
|
||||||
|
---@param active boolean
|
||||||
|
---@param hovered boolean
|
||||||
|
---@param x number
|
||||||
|
---@param y number
|
||||||
|
---@param w number
|
||||||
|
---@param h number
|
||||||
|
function TreeList:draw_item_text(item, active, hovered, x, y, w, h)
|
||||||
|
local item_text, item_font, item_color = self:get_item_text(item, active, hovered)
|
||||||
|
return common.draw_text(item_font, item_color, item_text, "left", x, y, 0, h)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param item widget.treelist.item
|
||||||
|
---@param active boolean
|
||||||
|
---@param hovered boolean
|
||||||
|
---@param x number
|
||||||
|
---@param y number
|
||||||
|
---@param w number
|
||||||
|
---@param h number
|
||||||
|
function TreeList:draw_item_icon(item, active, hovered, x, y, w, h)
|
||||||
|
local icon_char, icon_font, icon_color = self:get_item_icon(item, active, hovered)
|
||||||
|
if not icon_char then return 0 end
|
||||||
|
common.draw_text(icon_font, icon_color, icon_char, "left", x, y, 0, h)
|
||||||
|
return self.item_icon_width + self.item_text_spacing
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param item widget.treelist.item
|
||||||
|
---@param active boolean
|
||||||
|
---@param hovered boolean
|
||||||
|
---@param x number
|
||||||
|
---@param y number
|
||||||
|
---@param w number
|
||||||
|
---@param h number
|
||||||
|
function TreeList:draw_item_body(item, active, hovered, x, y, w, h)
|
||||||
|
x = x + self:draw_item_icon(item, active, hovered, x, y, w, h)
|
||||||
|
return self:draw_item_text(item, active, hovered, x, y, w, h)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param item widget.treelist.item
|
||||||
|
---@param active boolean
|
||||||
|
---@param hovered boolean
|
||||||
|
---@param x number
|
||||||
|
---@param y number
|
||||||
|
---@param w number
|
||||||
|
---@param h number
|
||||||
|
function TreeList:draw_item_chevron(item, active, hovered, x, y, w, h)
|
||||||
|
if item.childs and #item.childs > 0 then
|
||||||
|
local chevron_icon = item.expanded and "-" or "+"
|
||||||
|
local chevron_color = hovered and style.accent or style.text
|
||||||
|
common.draw_text(style.icon_font, chevron_color, chevron_icon, "left", x, y, 0, h)
|
||||||
|
end
|
||||||
|
return style.padding.x
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get an item chevron starting and ending positions.
|
||||||
|
---@return number x1
|
||||||
|
---@return number x2
|
||||||
|
function TreeList:get_chevron_position(item)
|
||||||
|
local ox = self:get_content_offset()
|
||||||
|
local x1 = ox + item.depth * style.padding.x
|
||||||
|
local x2 = x1 + style.padding.x * 2
|
||||||
|
return x1, x2
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param item widget.treelist.item
|
||||||
|
---@param active boolean
|
||||||
|
---@param hovered boolean
|
||||||
|
---@param x number
|
||||||
|
---@param y number
|
||||||
|
---@param w number
|
||||||
|
---@param h number
|
||||||
|
function TreeList:draw_item_background(item, active, hovered, x, y, w, h)
|
||||||
|
if hovered then
|
||||||
|
local hover_color = { table.unpack(style.line_highlight) }
|
||||||
|
hover_color[4] = 160
|
||||||
|
renderer.draw_rect(self.position.x, y, self.size.x, h, hover_color)
|
||||||
|
elseif active then
|
||||||
|
renderer.draw_rect(self.position.x, y, self.size.x, h, style.line_highlight)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param item widget.treelist.item
|
||||||
|
---@param active boolean
|
||||||
|
---@param hovered boolean
|
||||||
|
---@param x number
|
||||||
|
---@param y number
|
||||||
|
---@param w number
|
||||||
|
---@param h number
|
||||||
|
function TreeList:draw_item(item, active, hovered, x, y, w, h)
|
||||||
|
self:draw_item_background(item, active, hovered, x, y, w, h)
|
||||||
|
|
||||||
|
x = x + item.depth * style.padding.x + style.padding.x
|
||||||
|
x = x + self:draw_item_chevron(item, active, hovered, x, y, w, h)
|
||||||
|
|
||||||
|
return self:draw_item_body(item, active, hovered, x, y, w, h)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TreeList:draw()
|
||||||
|
if not TreeList.super.draw(self) then return end
|
||||||
|
|
||||||
|
local position, ox = self:get_position(), self:get_content_offset()
|
||||||
|
local _y, _h, sw = position.y, self.size.y, 0
|
||||||
|
for item, x,y,w,h in self:each_item() do
|
||||||
|
if y + h >= _y and y < _y + _h then
|
||||||
|
w = self:draw_item(
|
||||||
|
item,
|
||||||
|
item == self.selected_item,
|
||||||
|
item == self.hovered_item,
|
||||||
|
x, y, w, h
|
||||||
|
) - position.x + (position.x - ox)
|
||||||
|
sw = math.max(w, sw)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.scroll_width = sw
|
||||||
|
|
||||||
|
self:draw_scrollbar()
|
||||||
|
|
||||||
|
if
|
||||||
|
self.hovered_item and self.hovered_item.tooltip
|
||||||
|
and
|
||||||
|
self.tooltip.x and self.tooltip.alpha > 0
|
||||||
|
then
|
||||||
|
core.root_view:defer_draw(self.draw_tooltip, self)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param item? widget.treelist.item
|
||||||
|
---@param where integer
|
||||||
|
---@return widget.treelist.item item
|
||||||
|
---@return number x
|
||||||
|
---@return number y
|
||||||
|
---@return number w
|
||||||
|
---@return number h
|
||||||
|
function TreeList:get_item(item, where)
|
||||||
|
item = item or self.selected_item
|
||||||
|
|
||||||
|
local last_item, last_x, last_y, last_w, last_h
|
||||||
|
local stop = false
|
||||||
|
|
||||||
|
for it, x, y, w, h in self:each_item() do
|
||||||
|
if not item and where >= 0 then
|
||||||
|
return it, x, y, w, h
|
||||||
|
end
|
||||||
|
if item == it then
|
||||||
|
if where < 0 and last_item then
|
||||||
|
break
|
||||||
|
elseif where == 0 or (where < 0 and not last_item) then
|
||||||
|
return it, x, y, w, h
|
||||||
|
end
|
||||||
|
stop = true
|
||||||
|
elseif stop then
|
||||||
|
item = it
|
||||||
|
return it, x, y, w, h
|
||||||
|
end
|
||||||
|
last_item, last_x, last_y, last_w, last_h = it, x, y, w, h
|
||||||
|
end
|
||||||
|
return last_item, last_x, last_y, last_w, last_h
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param item? widget.treelist.item
|
||||||
|
---@return widget.treelist.item item
|
||||||
|
---@return number x
|
||||||
|
---@return number y
|
||||||
|
---@return number w
|
||||||
|
---@return number h
|
||||||
|
function TreeList:get_next(item)
|
||||||
|
return self:get_item(item, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param item? widget.treelist.item
|
||||||
|
---@return widget.treelist.item item
|
||||||
|
---@return number x
|
||||||
|
---@return number y
|
||||||
|
---@return number w
|
||||||
|
---@return number h
|
||||||
|
function TreeList:get_previous(item)
|
||||||
|
return self:get_item(item, -1)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TreeList:select_next()
|
||||||
|
self.selected_item = self:get_next()
|
||||||
|
end
|
||||||
|
|
||||||
|
function TreeList:select_prev()
|
||||||
|
self.selected_item = self:get_previous()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Expand or collapse the currently selected or given item.
|
||||||
|
---@param toggle? boolean
|
||||||
|
---@param item? widget.treelist.item
|
||||||
|
function TreeList:toggle_expand(toggle, item)
|
||||||
|
item = item or self.selected_item
|
||||||
|
|
||||||
|
if not item then return end
|
||||||
|
|
||||||
|
if item.childs and #item.childs > 0 then
|
||||||
|
if type(toggle) == "boolean" then
|
||||||
|
item.expanded = toggle
|
||||||
|
else
|
||||||
|
item.expanded = not item.expanded
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return TreeList
|
||||||
+278
@@ -0,0 +1,278 @@
|
|||||||
|
--- mod-version:3
|
||||||
|
local core = require "core"
|
||||||
|
local style = require "core.style"
|
||||||
|
local command = require "core.command"
|
||||||
|
local keymap = require "core.keymap"
|
||||||
|
local DocView = require "core.docview"
|
||||||
|
local config = require "core.config"
|
||||||
|
local common = require "core.common"
|
||||||
|
|
||||||
|
-- Colors can be configured as follows:
|
||||||
|
-- underline color = `style.bracketmatch_color`
|
||||||
|
-- bracket color = `style.bracketmatch_char_color`
|
||||||
|
-- background color = `style.bracketmatch_block_color`
|
||||||
|
-- frame color = `style.bracketmatch_frame_color`
|
||||||
|
|
||||||
|
config.plugins.bracketmatch = common.merge({
|
||||||
|
-- highlight the current bracket too
|
||||||
|
highlight_both = true,
|
||||||
|
-- can be "underline", "block", "frame", "none"
|
||||||
|
style = "underline",
|
||||||
|
-- color the bracket
|
||||||
|
color_char = false,
|
||||||
|
-- the size of the lines used in "underline" and "frame"
|
||||||
|
line_size = math.ceil(1 * SCALE),
|
||||||
|
-- The config specification used by the settings gui
|
||||||
|
config_spec = {
|
||||||
|
name = "Bracket Match",
|
||||||
|
{
|
||||||
|
label = "Highlight Both",
|
||||||
|
description = "Highlight the current bracket too.",
|
||||||
|
path = "highlight_both",
|
||||||
|
type = "toggle",
|
||||||
|
default = true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label = "Style",
|
||||||
|
description = "The visual indicator for pair brackets.",
|
||||||
|
path = "style",
|
||||||
|
type = "selection",
|
||||||
|
default = "underline",
|
||||||
|
values = {
|
||||||
|
{"Underline", "underline"},
|
||||||
|
{"Block", "block"},
|
||||||
|
{"Frame", "frame"},
|
||||||
|
{"None", "none"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label = "Colorize Bracket",
|
||||||
|
description = "Change the color of the matching brackets.",
|
||||||
|
path = "color_char",
|
||||||
|
type = "toggle",
|
||||||
|
default = false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label = "Line Size",
|
||||||
|
description = "Height of the underline on matching brackets.",
|
||||||
|
path = "line_size",
|
||||||
|
type = "number",
|
||||||
|
default = 1,
|
||||||
|
min = 1,
|
||||||
|
step = 1,
|
||||||
|
get_value = function(value)
|
||||||
|
return math.floor(value / SCALE)
|
||||||
|
end,
|
||||||
|
set_value = function(value)
|
||||||
|
return math.ceil(value * SCALE)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, config.plugins.bracketmatch)
|
||||||
|
|
||||||
|
|
||||||
|
local bracket_maps = {
|
||||||
|
-- [ ] ( ) { }
|
||||||
|
{ [91] = 93, [40] = 41, [123] = 125, direction = 1 },
|
||||||
|
-- ] [ ) ( } {
|
||||||
|
{ [93] = 91, [41] = 40, [125] = 123, direction = -1 },
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
local function get_token_at(doc, line, col)
|
||||||
|
local column = 0
|
||||||
|
for _,type,text in doc.highlighter:each_token(line) do
|
||||||
|
column = column + #text
|
||||||
|
if column >= col then return type, text end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function get_matching_bracket(doc, line, col, line_limit, open_byte, close_byte, direction)
|
||||||
|
local end_line = line + line_limit * direction
|
||||||
|
local depth = 0
|
||||||
|
|
||||||
|
while line ~= end_line do
|
||||||
|
local byte = doc.lines[line]:byte(col)
|
||||||
|
if byte == open_byte and get_token_at(doc, line, col) ~= "comment" then
|
||||||
|
depth = depth + 1
|
||||||
|
elseif byte == close_byte and get_token_at(doc, line, col) ~= "comment" then
|
||||||
|
depth = depth - 1
|
||||||
|
if depth == 0 then return line, col end
|
||||||
|
end
|
||||||
|
|
||||||
|
local prev_line, prev_col = line, col
|
||||||
|
line, col = doc:position_offset(line, col, direction)
|
||||||
|
if line == prev_line and col == prev_col then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local state = {}
|
||||||
|
local select_adj = 0
|
||||||
|
|
||||||
|
local function update_state(line_limit)
|
||||||
|
line_limit = line_limit or math.huge
|
||||||
|
|
||||||
|
-- reset if we don't have a document (eg. DocView isn't focused)
|
||||||
|
local doc = core.active_view.doc
|
||||||
|
if not doc then
|
||||||
|
state = {}
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- early exit if nothing has changed since the last call
|
||||||
|
local line, col = doc:get_selection()
|
||||||
|
local change_id = doc:get_change_id()
|
||||||
|
if state.doc == doc and state.line == line and state.col == col
|
||||||
|
and state.change_id == change_id and state.limit == line_limit then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- find matching bracket if we're on a bracket
|
||||||
|
local line2, col2
|
||||||
|
for _, map in ipairs(bracket_maps) do
|
||||||
|
for i = 0, -1, -1 do
|
||||||
|
local line, col = doc:position_offset(line, col, i)
|
||||||
|
local open = doc.lines[line]:byte(col)
|
||||||
|
local close = map[open]
|
||||||
|
if close and get_token_at(doc, line, col) ~= "comment" then
|
||||||
|
-- i == 0 if the cursor is on the left side of a bracket (or -1 when on right)
|
||||||
|
select_adj = i + 1 -- if i == 0 then select_adj = 1 else select_adj = 0 end
|
||||||
|
line2, col2 = get_matching_bracket(doc, line, col, line_limit, open, close, map.direction)
|
||||||
|
goto found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
::found::
|
||||||
|
|
||||||
|
-- update
|
||||||
|
state = {
|
||||||
|
change_id = change_id,
|
||||||
|
doc = doc,
|
||||||
|
line = line,
|
||||||
|
col = col,
|
||||||
|
line2 = line2,
|
||||||
|
col2 = col2,
|
||||||
|
limit = line_limit,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local update = DocView.update
|
||||||
|
|
||||||
|
function DocView:update(...)
|
||||||
|
update(self, ...)
|
||||||
|
update_state(100)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function redraw_char(dv, x, y, line, col, bg_color, char_color)
|
||||||
|
local x1 = x + dv:get_col_x_offset(line, col)
|
||||||
|
local x2 = x + dv:get_col_x_offset(line, col + 1)
|
||||||
|
local lh = dv:get_line_height()
|
||||||
|
local token = get_token_at(dv.doc, line, col)
|
||||||
|
if not char_color then
|
||||||
|
char_color = style.syntax[token]
|
||||||
|
end
|
||||||
|
local font = style.syntax_fonts[token] or dv:get_font()
|
||||||
|
local char = string.sub(dv.doc.lines[line], col, col)
|
||||||
|
|
||||||
|
if not bg_color then
|
||||||
|
-- redraw background
|
||||||
|
core.push_clip_rect(x1, y, x2 - x1, lh)
|
||||||
|
local dlt = DocView.draw_line_text
|
||||||
|
DocView.draw_line_text = function() end
|
||||||
|
dv:draw_line_body(line, x, y)
|
||||||
|
DocView.draw_line_text = dlt
|
||||||
|
core.pop_clip_rect()
|
||||||
|
else
|
||||||
|
renderer.draw_rect(x1, y, x2 - x1, lh, bg_color)
|
||||||
|
end
|
||||||
|
renderer.draw_text(font, char, x1, y + dv:get_line_text_y_offset(), char_color)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function draw_decoration(dv, x, y, line, col, width)
|
||||||
|
local conf = config.plugins.bracketmatch
|
||||||
|
local color = style.bracketmatch_color or style.syntax["function"]
|
||||||
|
local char_color = style.bracketmatch_char_color
|
||||||
|
or (conf.style == "block" and style.background or style.syntax["keyword"])
|
||||||
|
local block_color = style.bracketmatch_block_color or style.line_number2
|
||||||
|
local frame_color = style.bracketmatch_frame_color or style.line_number2
|
||||||
|
|
||||||
|
local h = conf.line_size
|
||||||
|
|
||||||
|
if conf.color_char or conf.style == "block" then
|
||||||
|
for i = 1, width, 1 do
|
||||||
|
redraw_char(dv, x, y, line, col + i - 1,
|
||||||
|
conf.style == "block" and block_color, conf.color_char and char_color)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if conf.style == "underline" then
|
||||||
|
local x1 = x + dv:get_col_x_offset(line, col)
|
||||||
|
local x2 = x + dv:get_col_x_offset(line, col + width)
|
||||||
|
local lh = dv:get_line_height()
|
||||||
|
|
||||||
|
renderer.draw_rect(x1, y + lh - h, x2 - x1, h, color)
|
||||||
|
elseif conf.style == "frame" then
|
||||||
|
local x1 = x + dv:get_col_x_offset(line, col)
|
||||||
|
local x2 = x + dv:get_col_x_offset(line, col + width)
|
||||||
|
local lh = dv:get_line_height()
|
||||||
|
|
||||||
|
renderer.draw_rect(x1, y + lh - h, x2 - x1, h, frame_color)
|
||||||
|
renderer.draw_rect(x1, y, x2 - x1, h, frame_color)
|
||||||
|
renderer.draw_rect(x1, y, h, lh, frame_color)
|
||||||
|
renderer.draw_rect(x2, y, h, lh, frame_color)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local draw_line_text = DocView.draw_line_text
|
||||||
|
|
||||||
|
function DocView:draw_line_text(line, x, y)
|
||||||
|
local lh = draw_line_text(self, line, x, y)
|
||||||
|
local width = 1
|
||||||
|
if self.doc == state.doc and state.line2 then
|
||||||
|
if line == state.line and config.plugins.bracketmatch.highlight_both then
|
||||||
|
local offset = 0
|
||||||
|
if state.line == state.line2 and math.abs(state.col + select_adj - 1 - state.col2) == 1 then
|
||||||
|
width = 2
|
||||||
|
if state.col > state.col2 then
|
||||||
|
offset = -1
|
||||||
|
end
|
||||||
|
if state.col2 > state.col + select_adj then
|
||||||
|
offset = 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
draw_decoration(self, x, y, line, state.col + select_adj + offset - 1, width)
|
||||||
|
end
|
||||||
|
if line == state.line2 and width == 1 then
|
||||||
|
draw_decoration(self, x, y, line, state.col2, width)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return lh
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
command.add("core.docview", {
|
||||||
|
["bracket-match:move-to-matching"] = function(dv)
|
||||||
|
update_state()
|
||||||
|
if state.line2 then
|
||||||
|
dv.doc:set_selection(state.line2, state.col2)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
["bracket-match:select-to-matching"] = function(dv)
|
||||||
|
update_state()
|
||||||
|
if state.line2 then
|
||||||
|
dv.doc:set_selection(state.line, state.col, state.line2, state.col2 + select_adj)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
keymap.add {
|
||||||
|
["ctrl+m"] = "bracket-match:move-to-matching",
|
||||||
|
["ctrl+shift+m"] = "bracket-match:select-to-matching",
|
||||||
|
}
|
||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
local config = require "core.config"
|
||||||
|
local common = require "core.common"
|
||||||
|
local DocView = require "core.docview"
|
||||||
|
|
||||||
|
|
||||||
|
config.plugins.colorpreview = common.merge({
|
||||||
|
enabled = true,
|
||||||
|
-- The config specification used by the settings gui
|
||||||
|
config_spec = {
|
||||||
|
name = "Color Preview",
|
||||||
|
{
|
||||||
|
label = "Enable",
|
||||||
|
description = "Enable or disable the color preview feature.",
|
||||||
|
path = "enabled",
|
||||||
|
type = "toggle",
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, config.plugins.colorpreview)
|
||||||
|
|
||||||
|
local white = { common.color "#ffffff" }
|
||||||
|
local black = { common.color "#000000" }
|
||||||
|
local tmp = {}
|
||||||
|
|
||||||
|
|
||||||
|
local function draw_color_previews(self, line, x, y, ptn, base, nibbles)
|
||||||
|
local text = self.doc.lines[line]
|
||||||
|
local s, e = 0, 0
|
||||||
|
|
||||||
|
while true do
|
||||||
|
s, e = text:find(ptn, e + 1)
|
||||||
|
if not s then break end
|
||||||
|
|
||||||
|
local str = text:sub(s, e)
|
||||||
|
local r, g, b, a = str:match(ptn)
|
||||||
|
r, g, b = tonumber(r, base), tonumber(g, base), tonumber(b, base)
|
||||||
|
a = tonumber(a or "", base)
|
||||||
|
if a ~= nil then
|
||||||
|
if base ~= 16 then
|
||||||
|
a = a * 0xff
|
||||||
|
end
|
||||||
|
else
|
||||||
|
a = 0xff
|
||||||
|
end
|
||||||
|
|
||||||
|
-- #123 becomes #112233
|
||||||
|
if nibbles then
|
||||||
|
r = r * 16
|
||||||
|
g = g * 16
|
||||||
|
b = b * 16
|
||||||
|
end
|
||||||
|
|
||||||
|
local x1 = x + self:get_col_x_offset(line, s)
|
||||||
|
local x2 = x + self:get_col_x_offset(line, e + 1)
|
||||||
|
local oy = self:get_line_text_y_offset()
|
||||||
|
|
||||||
|
local text_color = math.max(r, g, b) < 128 and white or black
|
||||||
|
tmp[1], tmp[2], tmp[3], tmp[4] = r, g, b, a
|
||||||
|
|
||||||
|
local l1, _, l2, _ = self.doc:get_selection(true)
|
||||||
|
|
||||||
|
if not (self.doc:has_selection() and line >= l1 and line <= l2) then
|
||||||
|
renderer.draw_rect(x1, y, x2 - x1, self:get_line_height(), tmp)
|
||||||
|
renderer.draw_text(self:get_font(), str, x1, y + oy, text_color)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local draw_line_text = DocView.draw_line_text
|
||||||
|
|
||||||
|
function DocView:draw_line_text(line, x, y)
|
||||||
|
local lh = draw_line_text(self, line, x, y)
|
||||||
|
if config.plugins.colorpreview.enabled then
|
||||||
|
draw_color_previews(self, line, x, y,
|
||||||
|
"#(%x%x)(%x%x)(%x%x)(%x?%x?)%f[%W]",
|
||||||
|
16
|
||||||
|
)
|
||||||
|
-- support #fff css format
|
||||||
|
draw_color_previews(self, line, x, y, "#(%x)(%x)(%x)%f[%W]", 16, true)
|
||||||
|
draw_color_previews(self, line, x, y,
|
||||||
|
"rgba?%((%d+)%D+(%d+)%D+(%d+)[%s,]-([%.%d]-)%s-%)",
|
||||||
|
nil
|
||||||
|
)
|
||||||
|
end
|
||||||
|
return lh
|
||||||
|
end
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
# EditorConfig
|
||||||
|
|
||||||
|
This plugin implements the [EditorConfig](https://editorconfig.org/) spec
|
||||||
|
purely on lua by leveraging lua patterns and the regex engine on lite-xl.
|
||||||
|
Installing additional dependencies is not required.
|
||||||
|
|
||||||
|
The EditorConfig spec was implemented as best understood,
|
||||||
|
if you find any bugs please report them on this repository
|
||||||
|
[issue tracker](https://github.com/lite-xl/lite-xl-plugins/issues).
|
||||||
|
|
||||||
|
## Implemented Features
|
||||||
|
|
||||||
|
Global options:
|
||||||
|
|
||||||
|
* root - prevents upward searching of .editorconfig files
|
||||||
|
|
||||||
|
Applied to documents indent info:
|
||||||
|
|
||||||
|
* indent_style
|
||||||
|
* indent_size
|
||||||
|
* tab_width
|
||||||
|
|
||||||
|
Applied on document save:
|
||||||
|
|
||||||
|
* end_of_line - if set to `cr` it is ignored
|
||||||
|
* trim_trailing_whitespace
|
||||||
|
* insert_final_newline boolean
|
||||||
|
|
||||||
|
## Not implemented
|
||||||
|
|
||||||
|
* charset - this feature would need the encoding
|
||||||
|
[PR](https://github.com/lite-xl/lite-xl/pull/1161) or
|
||||||
|
[plugin](https://github.com/jgmdev/lite-xl-encoding)
|
||||||
|
|
||||||
|
## Extras
|
||||||
|
|
||||||
|
* Supports multiple project directories
|
||||||
|
* Implements hot reloading, so modifying an .editorconfig file from within
|
||||||
|
the editor will re-apply all rules to currently opened files.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
This plugin includes a test suite to check how well the .editorconfig parser
|
||||||
|
is working.
|
||||||
|
|
||||||
|
The [editorconfig-core-test](https://github.com/editorconfig/editorconfig-core-test)
|
||||||
|
glob, parser and properties cmake tests where ported and we are getting a 100%
|
||||||
|
pass rate.
|
||||||
|
|
||||||
|
If you are interested in running the test suite, from the terminal execute
|
||||||
|
the following:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
lite-xl test editorconfig
|
||||||
|
```
|
||||||
|
|
||||||
|
To inspect the generated sections and regex rules:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
lite-xl test editorconfig --parsers
|
||||||
|
```
|
||||||
+441
@@ -0,0 +1,441 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
--
|
||||||
|
-- EditorConfig plugin for Lite XL
|
||||||
|
-- @copyright Jefferson Gonzalez <jgmdev@gmail.com>
|
||||||
|
-- @license MIT
|
||||||
|
--
|
||||||
|
-- Note: this plugin needs to be loaded after detectindent plugin,
|
||||||
|
-- since the name editorconfig.lua is ordered after detectindent.lua
|
||||||
|
-- there shouldn't be any issues. Just a reminder for the future in
|
||||||
|
-- case of a plugin that could also handle document identation type
|
||||||
|
-- and size, and has a name with more weight than this plugin.
|
||||||
|
--
|
||||||
|
local core = require "core"
|
||||||
|
local common = require "core.common"
|
||||||
|
local config = require "core.config"
|
||||||
|
local trimwhitespace = require "plugins.trimwhitespace"
|
||||||
|
local Doc = require "core.doc"
|
||||||
|
local Parser = require "plugins.editorconfig.parser"
|
||||||
|
|
||||||
|
---@class config.plugins.editorconfig
|
||||||
|
---@field debug boolean
|
||||||
|
config.plugins.editorconfig = common.merge({
|
||||||
|
debug = false,
|
||||||
|
-- The config specification used by the settings gui
|
||||||
|
config_spec = {
|
||||||
|
name = "EditorConfig",
|
||||||
|
{
|
||||||
|
label = "Debug",
|
||||||
|
description = "Display debugging messages on the log.",
|
||||||
|
path = "debug",
|
||||||
|
type = "toggle",
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, config.plugins.editorconfig)
|
||||||
|
|
||||||
|
---Cache of .editorconfig options to reduce parsing for every opened file.
|
||||||
|
---@type table<string, plugins.editorconfig.parser>
|
||||||
|
local project_configs = {}
|
||||||
|
|
||||||
|
---Keep track of main project directory so when changed we can assign a new
|
||||||
|
---.editorconfig object if neccesary.
|
||||||
|
---@type string
|
||||||
|
local main_project = core.project_dir
|
||||||
|
|
||||||
|
---Functionality that will be exposed by the plugin.
|
||||||
|
---@class plugins.editorconfig
|
||||||
|
local editorconfig = {}
|
||||||
|
|
||||||
|
---Load global .editorconfig options for a project.
|
||||||
|
---@param project_dir string
|
||||||
|
---@return boolean loaded
|
||||||
|
function editorconfig.load(project_dir)
|
||||||
|
local editor_config = project_dir .. "/" .. ".editorconfig"
|
||||||
|
local file = io.open(editor_config)
|
||||||
|
if file then
|
||||||
|
file:close()
|
||||||
|
project_configs[project_dir] = Parser.new(editor_config)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
---Helper to add or substract final new line, it also makes final new line
|
||||||
|
---visble which lite-xl does not.
|
||||||
|
---@param doc core.doc
|
||||||
|
---@param raw? boolean If true does not register change on undo stack
|
||||||
|
---@return boolean handled_new_line
|
||||||
|
local function handle_final_new_line(doc, raw)
|
||||||
|
local handled = false
|
||||||
|
---@diagnostic disable-next-line
|
||||||
|
if doc.insert_final_newline then
|
||||||
|
handled = true
|
||||||
|
if doc.lines[#doc.lines] ~= "\n" then
|
||||||
|
if not raw then
|
||||||
|
doc:insert(#doc.lines, math.huge, "\n")
|
||||||
|
else
|
||||||
|
table.insert(doc.lines, "\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
---@diagnostic disable-next-line
|
||||||
|
elseif type(doc.insert_final_newline) == "boolean" then
|
||||||
|
handled = true
|
||||||
|
if trimwhitespace.trim_empty_end_lines then
|
||||||
|
trimwhitespace.trim_empty_end_lines(doc, raw)
|
||||||
|
-- TODO: remove this once 2.1.1 is released
|
||||||
|
else
|
||||||
|
for _=#doc.lines, 1, -1 do
|
||||||
|
local l = #doc.lines
|
||||||
|
if l > 1 and doc.lines[l] == "\n" then
|
||||||
|
local current_line = doc:get_selection()
|
||||||
|
if current_line == l then
|
||||||
|
doc:set_selection(l-1, math.huge, l-1, math.huge)
|
||||||
|
end
|
||||||
|
if not raw then
|
||||||
|
doc:remove(l-1, math.huge, l, math.huge)
|
||||||
|
else
|
||||||
|
table.remove(doc.lines, l)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return handled
|
||||||
|
end
|
||||||
|
|
||||||
|
---Split the given relative path by / or \ separators.
|
||||||
|
---@param path string The path to split
|
||||||
|
---@return table
|
||||||
|
local function split_path(path)
|
||||||
|
local result = {};
|
||||||
|
for match in (path.."/"):gmatch("(.-)".."[\\/]") do
|
||||||
|
table.insert(result, match);
|
||||||
|
end
|
||||||
|
return result;
|
||||||
|
end
|
||||||
|
|
||||||
|
---Check if the given file path exists.
|
||||||
|
---@param file_path string
|
||||||
|
local function file_exists(file_path)
|
||||||
|
local file = io.open(file_path, "r")
|
||||||
|
if not file then return false end
|
||||||
|
file:close()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
---Merge a config options to target if they don't already exists on target.
|
||||||
|
---@param config_target? plugins.editorconfig.parser.section
|
||||||
|
---@param config_from? plugins.editorconfig.parser.section
|
||||||
|
local function merge_config(config_target, config_from)
|
||||||
|
if config_target and config_from then
|
||||||
|
for name, value in pairs(config_from) do
|
||||||
|
if type(config_target[name]) == "nil" then
|
||||||
|
config_target[name] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Scan for .editorconfig files from current file path to upper project path
|
||||||
|
---if root attribute is not found first and returns matching config.
|
||||||
|
---@param file_path string
|
||||||
|
---@return plugins.editorconfig.parser.section?
|
||||||
|
local function recursive_get_config(file_path)
|
||||||
|
local project_dir = ""
|
||||||
|
|
||||||
|
local root_config
|
||||||
|
for path, editor_config in pairs(project_configs) do
|
||||||
|
if common.path_belongs_to(file_path, path) then
|
||||||
|
project_dir = path
|
||||||
|
root_config = editor_config:getConfig(
|
||||||
|
common.relative_path(path, file_path)
|
||||||
|
)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if project_dir == "" then
|
||||||
|
for _, project in ipairs(core.project_directories) do
|
||||||
|
if common.path_belongs_to(file_path, project.name) then
|
||||||
|
project_dir = project.name
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local relative_file_path = common.relative_path(project_dir, file_path)
|
||||||
|
local dir = common.dirname(relative_file_path)
|
||||||
|
|
||||||
|
local editor_config = {}
|
||||||
|
local config_found = false
|
||||||
|
if not dir and root_config then
|
||||||
|
editor_config = root_config
|
||||||
|
config_found = true
|
||||||
|
elseif dir then
|
||||||
|
local path_list = split_path(dir)
|
||||||
|
local root_found = false
|
||||||
|
for p=#path_list, 1, -1 do
|
||||||
|
local path = project_dir .. "/" .. table.concat(path_list, "/", 1, p)
|
||||||
|
if file_exists(path .. "/" .. ".editorconfig") then
|
||||||
|
---@type plugins.editorconfig.parser
|
||||||
|
local parser = Parser.new(path .. "/" .. ".editorconfig")
|
||||||
|
local pconfig = parser:getConfig(common.relative_path(path, file_path))
|
||||||
|
if pconfig then
|
||||||
|
merge_config(editor_config, pconfig)
|
||||||
|
config_found = true
|
||||||
|
end
|
||||||
|
if parser.root then
|
||||||
|
root_found = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not root_found and root_config then
|
||||||
|
merge_config(editor_config, root_config)
|
||||||
|
config_found = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- clean unset options
|
||||||
|
if config_found then
|
||||||
|
local all_unset = true
|
||||||
|
for name, value in pairs(editor_config) do
|
||||||
|
if value == "unset" then
|
||||||
|
editor_config[name] = nil
|
||||||
|
else
|
||||||
|
all_unset = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if all_unset then config_found = false end
|
||||||
|
end
|
||||||
|
|
||||||
|
return config_found and editor_config or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---Apply editorconfig rules to given doc if possible.
|
||||||
|
---@param doc core.doc
|
||||||
|
function editorconfig.apply(doc)
|
||||||
|
if not doc.abs_filename and not doc.filename then return end
|
||||||
|
local file_path = doc.abs_filename or (main_project .. "/" .. doc.filename)
|
||||||
|
local options = recursive_get_config(file_path)
|
||||||
|
if options then
|
||||||
|
if config.plugins.editorconfig.debug then
|
||||||
|
core.log_quiet(
|
||||||
|
"[EditorConfig]: %s applied %s",
|
||||||
|
file_path, common.serialize(options, {pretty = true})
|
||||||
|
)
|
||||||
|
end
|
||||||
|
local indent_type, indent_size = doc:get_indent_info()
|
||||||
|
if options.indent_style then
|
||||||
|
if options.indent_style == "tab" then
|
||||||
|
indent_type = "hard"
|
||||||
|
else
|
||||||
|
indent_type = "soft"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if options.indent_size and options.indent_size == "tab" then
|
||||||
|
if options.tab_width then
|
||||||
|
options.indent_size = options.tab_width
|
||||||
|
else
|
||||||
|
options.indent_size = config.indent_size or 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if options.indent_size then
|
||||||
|
indent_size = options.indent_size
|
||||||
|
end
|
||||||
|
|
||||||
|
if doc.indent_info then
|
||||||
|
doc.indent_info.type = indent_type
|
||||||
|
doc.indent_info.size = indent_size
|
||||||
|
doc.indent_info.confirmed = true
|
||||||
|
else
|
||||||
|
doc.indent_info = {
|
||||||
|
type = indent_type,
|
||||||
|
size = indent_size,
|
||||||
|
confirmed = true
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
if options.end_of_line then
|
||||||
|
if options.end_of_line == "crlf" then
|
||||||
|
doc.crlf = true
|
||||||
|
elseif options.end_of_line == "lf" then
|
||||||
|
doc.crlf = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if options.trim_trailing_whitespace then
|
||||||
|
doc.trim_trailing_whitespace = true
|
||||||
|
elseif options.trim_trailing_whitespace == false then
|
||||||
|
doc.trim_trailing_whitespace = false
|
||||||
|
else
|
||||||
|
doc.trim_trailing_whitespace = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if options.insert_final_newline then
|
||||||
|
doc.insert_final_newline = true
|
||||||
|
elseif options.insert_final_newline == false then
|
||||||
|
doc.insert_final_newline = false
|
||||||
|
else
|
||||||
|
doc.insert_final_newline = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if
|
||||||
|
(
|
||||||
|
type(doc.trim_trailing_whitespace) == "boolean"
|
||||||
|
or
|
||||||
|
type(doc.insert_final_newline) == "boolean"
|
||||||
|
)
|
||||||
|
-- TODO: remove this once 2.1.1 is released
|
||||||
|
and
|
||||||
|
trimwhitespace.disable
|
||||||
|
then
|
||||||
|
trimwhitespace.disable(doc)
|
||||||
|
end
|
||||||
|
|
||||||
|
handle_final_new_line(doc, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Applies .editorconfig options to all open documents if possible.
|
||||||
|
function editorconfig.apply_all()
|
||||||
|
for _, doc in ipairs(core.docs) do
|
||||||
|
editorconfig.apply(doc)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Load .editorconfig on all projects loaded at startup and apply it
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
core.add_thread(function()
|
||||||
|
local loaded = false
|
||||||
|
|
||||||
|
-- scan all opened project directories
|
||||||
|
if core.project_directories then
|
||||||
|
for i=1, #core.project_directories do
|
||||||
|
local found = editorconfig.load(core.project_directories[i].name)
|
||||||
|
if found then loaded = true end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- if an editorconfig was found then try to apply it to opened docs
|
||||||
|
if loaded then
|
||||||
|
editorconfig.apply_all()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Override various core project loading functions for .editorconfig scanning
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
local core_open_folder_project = core.open_folder_project
|
||||||
|
function core.open_folder_project(directory)
|
||||||
|
core_open_folder_project(directory)
|
||||||
|
if project_configs[main_project] then project_configs[main_project] = nil end
|
||||||
|
main_project = core.project_dir
|
||||||
|
editorconfig.load(main_project)
|
||||||
|
end
|
||||||
|
|
||||||
|
local core_remove_project_directory = core.remove_project_directory
|
||||||
|
function core.remove_project_directory(path)
|
||||||
|
local out = core_remove_project_directory(path)
|
||||||
|
if project_configs[path] then project_configs[path] = nil end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
local core_add_project_directory = core.add_project_directory
|
||||||
|
function core.add_project_directory(directory)
|
||||||
|
local out = core_add_project_directory(directory)
|
||||||
|
editorconfig.load(directory)
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Hook into the core.doc to apply editor config options
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
local doc_new = Doc.new
|
||||||
|
function Doc:new(...)
|
||||||
|
doc_new(self, ...)
|
||||||
|
editorconfig.apply(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Cloned trimwitespace plugin until it is exposed for other plugins.
|
||||||
|
---@param doc core.doc
|
||||||
|
local function trim_trailing_whitespace(doc)
|
||||||
|
if trimwhitespace.trim then
|
||||||
|
trimwhitespace.trim(doc)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- TODO: remove this once 2.1.1 is released
|
||||||
|
local cline, ccol = doc:get_selection()
|
||||||
|
for i = 1, #doc.lines do
|
||||||
|
local old_text = doc:get_text(i, 1, i, math.huge)
|
||||||
|
local new_text = old_text:gsub("%s*$", "")
|
||||||
|
|
||||||
|
-- don't remove whitespace which would cause the caret to reposition
|
||||||
|
if cline == i and ccol > #new_text then
|
||||||
|
new_text = old_text:sub(1, ccol - 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
if old_text ~= new_text then
|
||||||
|
doc:insert(i, 1, new_text)
|
||||||
|
doc:remove(i, #new_text + 1, i, math.huge)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local doc_save = Doc.save
|
||||||
|
function Doc:save(...)
|
||||||
|
local new_file = self.new_file
|
||||||
|
|
||||||
|
---@diagnostic disable-next-line
|
||||||
|
if self.trim_trailing_whitespace then
|
||||||
|
trim_trailing_whitespace(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
local lc = #self.lines
|
||||||
|
local handle_new_line = handle_final_new_line(self)
|
||||||
|
|
||||||
|
-- remove the unnecesary visible \n\n or the disabled \n
|
||||||
|
if handle_new_line then
|
||||||
|
self.lines[lc] = self.lines[lc]:gsub("\n$", "")
|
||||||
|
end
|
||||||
|
|
||||||
|
doc_save(self, ...)
|
||||||
|
|
||||||
|
-- restore the visible \n\n or disabled \n
|
||||||
|
if handle_new_line then
|
||||||
|
self.lines[lc] = self.lines[lc] .. "\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
if common.basename(self.abs_filename) == ".editorconfig" then
|
||||||
|
-- blindlessly reload related project .editorconfig options
|
||||||
|
for _, project in ipairs(core.project_directories) do
|
||||||
|
if common.path_belongs_to(self.abs_filename, project.name) then
|
||||||
|
editorconfig.load(project.name)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- re-apply editorconfig options to all open files
|
||||||
|
editorconfig.apply_all()
|
||||||
|
elseif new_file then
|
||||||
|
-- apply editorconfig options for file that was previously unsaved
|
||||||
|
editorconfig.apply(self)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Run the test suite if requested on CLI with: lite-xl test editorconfig
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
for i, argument in ipairs(ARGS) do
|
||||||
|
if argument == "test" and ARGS[i+1] == "editorconfig" then
|
||||||
|
require "plugins.editorconfig.runtest"
|
||||||
|
os.exit()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return editorconfig
|
||||||
+553
@@ -0,0 +1,553 @@
|
|||||||
|
-- Lua parser implementation of the .editorconfig spec as best understood.
|
||||||
|
-- @copyright Jefferson Gonzalez <jgmdev@gmail.com>
|
||||||
|
-- @license MIT
|
||||||
|
|
||||||
|
local core = require "core"
|
||||||
|
local config = require "core.config"
|
||||||
|
|
||||||
|
local STANDALONE = false
|
||||||
|
for i, argument in ipairs(ARGS) do
|
||||||
|
if argument == "test" and ARGS[i+1] == "editorconfig" then
|
||||||
|
STANDALONE = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Logger that will output using lite-xl logging functions or print to
|
||||||
|
---terminal if the parser is running in standalone mode.
|
||||||
|
---@param type "log" | "error"
|
||||||
|
---@param format string
|
||||||
|
---@param ... any
|
||||||
|
local function log(type, format, ...)
|
||||||
|
if not STANDALONE then
|
||||||
|
core[type]("[EditorConfig]: " .. format, ...)
|
||||||
|
else
|
||||||
|
print("[" .. type:upper() .. "]: " .. string.format(format, ...))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Represents an .editorconfig path rule/expression.
|
||||||
|
---@class plugins.editorconfig.parser.rule
|
||||||
|
---Path expression as found between square brackets.
|
||||||
|
---@field expression string | table<integer,string>
|
||||||
|
---The expression converted to a regex.
|
||||||
|
---@field regex string | table<integer,string>
|
||||||
|
---@field regex_compiled any? | table<integer,string>
|
||||||
|
---@field negation boolean Indicates that the expression is a negation.
|
||||||
|
---@field ranges table<integer,number> List of ranges found on the expression.
|
||||||
|
|
||||||
|
---Represents a section of the .editorconfig with all its config options.
|
||||||
|
---@class plugins.editorconfig.parser.section
|
||||||
|
---@field rule plugins.editorconfig.parser.rule
|
||||||
|
---@field equivalent_rules plugins.editorconfig.parser.rule[]
|
||||||
|
---@field indent_style "tab" | "space"
|
||||||
|
---@field indent_size integer
|
||||||
|
---@field tab_width integer
|
||||||
|
---@field end_of_line "lf" | "cr" | "crlf"
|
||||||
|
---@field charset "latin1" | "utf-8" | "utf-8-bom" | "utf-16be" | "utf-16le"
|
||||||
|
---@field trim_trailing_whitespace boolean
|
||||||
|
---@field insert_final_newline boolean
|
||||||
|
|
||||||
|
---EditorConfig parser class and filename config matching.
|
||||||
|
---@class plugins.editorconfig.parser
|
||||||
|
---@field config_path string
|
||||||
|
---@field sections plugins.editorconfig.parser.section[]
|
||||||
|
---@field root boolean
|
||||||
|
local Parser = {}
|
||||||
|
Parser.__index = Parser
|
||||||
|
|
||||||
|
---Constructor
|
||||||
|
---@param config_path string
|
||||||
|
---@return plugins.editorconfig.parser
|
||||||
|
function Parser.new(config_path)
|
||||||
|
local self = {}
|
||||||
|
setmetatable(self, Parser)
|
||||||
|
self.config_path = config_path
|
||||||
|
self.sections = {}
|
||||||
|
self.root = false
|
||||||
|
self:read()
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--- char to hex cache and automatic converter
|
||||||
|
---@type table<string,string>
|
||||||
|
local hex_value = {}
|
||||||
|
setmetatable(hex_value, {
|
||||||
|
__index = function(t, k)
|
||||||
|
local v = rawget(t, k)
|
||||||
|
if v == nil then
|
||||||
|
v = string.format("%x", string.byte(k))
|
||||||
|
rawset(t, k, v)
|
||||||
|
end
|
||||||
|
return v
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
---Simplifies managing rules with other inner rules like {...} which can
|
||||||
|
---contain escaped \\{ \\} and expressions that are easier handled after
|
||||||
|
---converting the escaped special characters to \xXX counterparts.
|
||||||
|
---@param value string
|
||||||
|
---@return string escaped_values
|
||||||
|
local function escapes_to_regex_hex(value)
|
||||||
|
local escaped_chars = {}
|
||||||
|
for char in value:ugmatch("\\(.)") do
|
||||||
|
table.insert(escaped_chars, char)
|
||||||
|
end
|
||||||
|
for _, char in ipairs(escaped_chars) do
|
||||||
|
value = value:ugsub("\\" .. char, "\\x" .. hex_value[char])
|
||||||
|
end
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
---An .editorconfig path expression to regex conversion rule.
|
||||||
|
---@class rule
|
||||||
|
---@field rule string Lua pattern.
|
||||||
|
---Callback conversion function.
|
||||||
|
---@field conversion fun(match:string, section:plugins.editorconfig.parser.section):string
|
||||||
|
|
||||||
|
---List of conversion rules applied to brace expressions.
|
||||||
|
---@type rule[]
|
||||||
|
local RULES_BRACES = {
|
||||||
|
{ rule = "^%(", conversion = function() return "\\(" end },
|
||||||
|
{ rule = "^%)", conversion = function() return "\\)" end },
|
||||||
|
{ rule = "^%.", conversion = function() return "\\." end },
|
||||||
|
{ rule = "^\\%[", conversion = function() return "\\[" end },
|
||||||
|
{ rule = "^\\%]", conversion = function() return "\\]" end },
|
||||||
|
{ rule = "^\\!", conversion = function() return "!" end },
|
||||||
|
{ rule = "^\\;", conversion = function() return ";" end },
|
||||||
|
{ rule = "^\\#", conversion = function() return "#" end },
|
||||||
|
{ rule = "^\\,", conversion = function() return "," end },
|
||||||
|
{ rule = "^\\{", conversion = function() return "{" end },
|
||||||
|
{ rule = "^\\}", conversion = function() return "}" end },
|
||||||
|
{ rule = "^,", conversion = function() return "|" end },
|
||||||
|
{ rule = "^\\%*", conversion = function() return "\\*" end },
|
||||||
|
{ rule = "^%*", conversion = function() return "[^\\/]*" end },
|
||||||
|
{ rule = "^%*%*", conversion = function() return ".*" end },
|
||||||
|
{ rule = "^%?", conversion = function() return "." end },
|
||||||
|
{ rule = "^{}", conversion = function() return "{}" end },
|
||||||
|
{ rule = "^{[^,]+}", conversion = function(match) return match end },
|
||||||
|
{ rule = "^%b{}",
|
||||||
|
conversion = function(match)
|
||||||
|
local out = match:ugsub("%(", "\\(")
|
||||||
|
:ugsub("%)", "\\)")
|
||||||
|
:ugsub("%.", "\\.")
|
||||||
|
:ugsub("\\%[", "[\\[]")
|
||||||
|
:ugsub("\\%]", "[\\]]")
|
||||||
|
:ugsub("^\\!", "!")
|
||||||
|
:ugsub("^\\;", ";")
|
||||||
|
:ugsub("^\\#", "#")
|
||||||
|
-- negation chars list
|
||||||
|
:ugsub("%[!(%a+)%]", "[^%1]")
|
||||||
|
:ugsub("\\\\", "[\\]")
|
||||||
|
-- escaped braces
|
||||||
|
:ugsub("\\{", "[{]")
|
||||||
|
:ugsub("\\}", "[}]")
|
||||||
|
-- non escaped braces
|
||||||
|
:ugsub("{([^%]])", "(%1")
|
||||||
|
:ugsub("}([^%]])", ")%1")
|
||||||
|
:ugsub("^{", "(")
|
||||||
|
:ugsub("}$", ")")
|
||||||
|
-- escaped globs
|
||||||
|
:ugsub("\\%*", "[\\*]")
|
||||||
|
:ugsub("\\%?", "[\\?]")
|
||||||
|
-- non escaped globs
|
||||||
|
:ugsub("%*%*", "[*][*]") -- prevent this glob from expanding to next sub
|
||||||
|
:ugsub("%*([^%]])", "[^\\/]*%1")
|
||||||
|
:ugsub("%[%*%]%[%*%]", ".*")
|
||||||
|
:ugsub("%?([^%]])", ".%1")
|
||||||
|
-- escaped comma
|
||||||
|
:ugsub("\\,", "[,]")
|
||||||
|
-- non escaped comma
|
||||||
|
:ugsub(",([^%]])", "|%1")
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
},
|
||||||
|
{ rule = "^%[[^/%]]*%]",
|
||||||
|
conversion = function(match)
|
||||||
|
local negation = match:umatch("^%[!")
|
||||||
|
local chars = match:umatch("^%[!?(.-)%]")
|
||||||
|
chars = chars:ugsub("^%-", "\\-"):ugsub("%-$", "\\-")
|
||||||
|
local out = ""
|
||||||
|
if negation then
|
||||||
|
out = "[^"..chars.."]"
|
||||||
|
else
|
||||||
|
out = "["..chars.."]"
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
---List of conversion rules applied to .editorconfig path expressions.
|
||||||
|
---@type rule[]
|
||||||
|
local RULES = {
|
||||||
|
-- normalize escaped .editorconfig special chars or keep them escaped
|
||||||
|
{ rule = "^\\x[a-fA-F][a-fA-F]", conversion = function(match) return match end },
|
||||||
|
{ rule = "^\\%*", conversion = function() return "\\*" end },
|
||||||
|
{ rule = "^\\%?", conversion = function() return "\\?" end },
|
||||||
|
{ rule = "^\\{", conversion = function() return "{" end },
|
||||||
|
{ rule = "^\\}", conversion = function() return "}" end },
|
||||||
|
{ rule = "^\\%[", conversion = function() return "\\[" end },
|
||||||
|
{ rule = "^\\%]", conversion = function() return "\\]" end },
|
||||||
|
{ rule = "^\\!", conversion = function() return "!" end },
|
||||||
|
{ rule = "^\\;", conversion = function() return ";" end },
|
||||||
|
{ rule = "^\\#", conversion = function() return "#" end },
|
||||||
|
-- escape special chars
|
||||||
|
{ rule = "^%.", conversion = function() return "\\." end },
|
||||||
|
{ rule = "^%(", conversion = function() return "\\(" end },
|
||||||
|
{ rule = "^%)", conversion = function() return "\\)" end },
|
||||||
|
{ rule = "^%[[^/%]]*%]",
|
||||||
|
conversion = function(match)
|
||||||
|
local negation = match:umatch("^%[!")
|
||||||
|
local chars = match:umatch("^%[!?(.-)%]")
|
||||||
|
chars = chars:ugsub("^%-", "\\-"):ugsub("%-$", "\\-")
|
||||||
|
local out = ""
|
||||||
|
if negation then
|
||||||
|
out = "[^"..chars.."]"
|
||||||
|
else
|
||||||
|
out = "["..chars.."]"
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
},
|
||||||
|
-- Is this negation rule valid?
|
||||||
|
{ rule = "^!%w+",
|
||||||
|
conversion = function(match)
|
||||||
|
local chars = match:umatch("%w+")
|
||||||
|
return "[^"..chars.."]"
|
||||||
|
end
|
||||||
|
},
|
||||||
|
-- escape square brackets
|
||||||
|
{ rule = "^%[", conversion = function() return "\\[" end },
|
||||||
|
{ rule = "^%]", conversion = function() return "\\]" end },
|
||||||
|
-- match any characters
|
||||||
|
{ rule = "^%*%*", conversion = function() return ".*" end },
|
||||||
|
-- match any characters excluding path separators, \ not needed but just in case
|
||||||
|
{ rule = "^%*", conversion = function() return "[^\\/]*" end },
|
||||||
|
-- match optional character, doesn't matters what or should only be a \w?
|
||||||
|
{ rule = "^%?", conversion = function() return "[^/]" end },
|
||||||
|
-- threat empty braces literally
|
||||||
|
{ rule = "^{}", conversion = function() return "{}" end },
|
||||||
|
-- match a number range
|
||||||
|
{ rule = "^{%-?%d+%.%.%-?%d+}",
|
||||||
|
conversion = function(match, section)
|
||||||
|
local min, max = match:umatch("(-?%d+)%.%.(-?%d+)")
|
||||||
|
min = tonumber(min)
|
||||||
|
max = tonumber(max)
|
||||||
|
if min and max then
|
||||||
|
if not section.rule.ranges then section.rule.ranges = {} end
|
||||||
|
table.insert(section.rule.ranges, {
|
||||||
|
math.min(min, max),
|
||||||
|
math.max(min, max)
|
||||||
|
})
|
||||||
|
end
|
||||||
|
local minus = ""
|
||||||
|
if min < 0 or max < 0 then minus = "\\-?" end
|
||||||
|
return "(?<!0)("..minus.."[1-9]\\d*)"
|
||||||
|
end
|
||||||
|
},
|
||||||
|
-- threat single option braces literally
|
||||||
|
{ rule = "^{[^,]+}", conversion = function(match) return match end },
|
||||||
|
-- match invalid range
|
||||||
|
{ rule = "^{[^%.]+%.%.[^%.]+}", conversion = function(match) return match end },
|
||||||
|
-- match any of the strings separated by commas inside the curly braces
|
||||||
|
{ rule = "^%b{}",
|
||||||
|
conversion = function(rule, section)
|
||||||
|
rule = rule:gsub("^{", ""):gsub("}$", "")
|
||||||
|
local pos, len, exp = 1, rule:ulen(), ""
|
||||||
|
|
||||||
|
while pos <= len do
|
||||||
|
local found = false
|
||||||
|
for _, r in ipairs(RULES_BRACES) do
|
||||||
|
local match = rule:umatch(r.rule, pos)
|
||||||
|
if match then
|
||||||
|
exp = exp .. r.conversion(match, section)
|
||||||
|
pos = pos + match:ulen()
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not found then
|
||||||
|
exp = exp .. rule:usub(pos, pos)
|
||||||
|
pos = pos + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return "(" .. exp .. ")"
|
||||||
|
end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
---Adds the regex equivalent of a section path expression.
|
||||||
|
---@param section plugins.editorconfig.parser.section | string
|
||||||
|
---@return plugins.editorconfig.parser.section
|
||||||
|
function Parser:rule_to_regex(section)
|
||||||
|
if type(section) == "string" then
|
||||||
|
section = {rule = {expression = section}}
|
||||||
|
end
|
||||||
|
|
||||||
|
local rule = section.rule.expression
|
||||||
|
|
||||||
|
-- match everything rule which is different from regular *
|
||||||
|
-- that doesn't matches path separators
|
||||||
|
if rule == "*" then
|
||||||
|
section.rule.regex = ".+"
|
||||||
|
section.rule.regex_compiled = regex.compile(".+")
|
||||||
|
return section
|
||||||
|
end
|
||||||
|
|
||||||
|
rule = escapes_to_regex_hex(section.rule.expression)
|
||||||
|
|
||||||
|
local pos, len, exp = 1, rule:ulen(), ""
|
||||||
|
|
||||||
|
-- if expression starts with ! it is treated entirely as a negation
|
||||||
|
local negation = rule:umatch("^%s*!")
|
||||||
|
if negation then
|
||||||
|
pos = pos + negation:ulen() + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- apply all conversion rules by looping the path expression/rule
|
||||||
|
while pos <= len do
|
||||||
|
local found = false
|
||||||
|
for _, r in ipairs(RULES) do
|
||||||
|
local match = rule:umatch(r.rule, pos)
|
||||||
|
if match then
|
||||||
|
exp = exp .. r.conversion(match, section)
|
||||||
|
pos = pos + match:ulen()
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not found then
|
||||||
|
exp = exp .. rule:usub(pos, pos)
|
||||||
|
pos = pos + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- force match up to the end
|
||||||
|
exp = exp .. "$"
|
||||||
|
|
||||||
|
-- allow expressions that start with * to match anything on start
|
||||||
|
if exp:match("^%[^\\/%]%*") then
|
||||||
|
exp = exp:gsub("^%[^\\/%]%*", ".*")
|
||||||
|
-- fixes two failing tests
|
||||||
|
elseif exp:match("^%[") then
|
||||||
|
exp = "^" .. exp
|
||||||
|
-- match only on root dir
|
||||||
|
elseif exp:match("^/") then
|
||||||
|
exp = exp:gsub("^/", "^")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- store changes to the section rule
|
||||||
|
section.rule.regex, section.rule.negation = exp, negation
|
||||||
|
section.rule.regex_compiled = regex.compile(section.rule.regex)
|
||||||
|
if not section.rule.regex_compiled then
|
||||||
|
log(
|
||||||
|
"error",
|
||||||
|
"could not compile '[%s]' to regex '%s'",
|
||||||
|
rule, section.rule.regex
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return section
|
||||||
|
end
|
||||||
|
|
||||||
|
---Parses the associated .editorconfig file and stores each section.
|
||||||
|
function Parser:read()
|
||||||
|
local file = io.open(self.config_path, "r")
|
||||||
|
|
||||||
|
self.sections = {}
|
||||||
|
|
||||||
|
if not file then
|
||||||
|
log("log", "could not read %s", self.config_path)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
---@type plugins.editorconfig.parser.section
|
||||||
|
local section = {}
|
||||||
|
|
||||||
|
for line in file:lines() do
|
||||||
|
---@cast line string
|
||||||
|
|
||||||
|
-- first we try to see if the line is a rule section
|
||||||
|
local rule = ""
|
||||||
|
rule = line:umatch("^%s*%[(.+)%]%s*$")
|
||||||
|
if rule then
|
||||||
|
if section.rule then
|
||||||
|
-- save previous section and crerate new one
|
||||||
|
table.insert(self.sections, section)
|
||||||
|
section = {}
|
||||||
|
end
|
||||||
|
section.rule = {
|
||||||
|
expression = rule
|
||||||
|
}
|
||||||
|
-- convert the expression to a regex directly on the section table
|
||||||
|
self:rule_to_regex(section)
|
||||||
|
|
||||||
|
local clone = rule
|
||||||
|
if clone:match("//+") or clone:match("/%*%*/") then
|
||||||
|
section.equivalent_rules = {}
|
||||||
|
end
|
||||||
|
while clone:match("//+") or clone:match("/%*%*/") do
|
||||||
|
---@type plugins.editorconfig.parser.section[]
|
||||||
|
if clone:match("//+") then
|
||||||
|
clone = clone:ugsub("//+", "/", 1)
|
||||||
|
table.insert(section.equivalent_rules, self:rule_to_regex(clone).rule)
|
||||||
|
end
|
||||||
|
if clone:match("/%*%*/") then
|
||||||
|
clone = clone:ugsub("/%*%*/", "/", 1)
|
||||||
|
table.insert(section.equivalent_rules, self:rule_to_regex(clone).rule)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not rule then
|
||||||
|
local name, value = line:umatch("^%s*(%w%S+)%s*=%s*([^\n\r]+)")
|
||||||
|
if name and value then
|
||||||
|
name = name:ulower()
|
||||||
|
-- do not lowercase property values that start with test_
|
||||||
|
if not name:match("^test_") then
|
||||||
|
value = value:ulower()
|
||||||
|
end
|
||||||
|
if value == "true" then
|
||||||
|
value = true
|
||||||
|
elseif value == "false" then
|
||||||
|
value = false
|
||||||
|
elseif math.tointeger and math.tointeger(value) then
|
||||||
|
value = math.tointeger(value)
|
||||||
|
elseif tonumber(value) then
|
||||||
|
value = tonumber(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
if section.rule then
|
||||||
|
section[name] = value
|
||||||
|
elseif name == "root" and type(value) == "boolean" then
|
||||||
|
self.root = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if section.rule then
|
||||||
|
table.insert(self.sections, section)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Helper function that converts a regex offset results into a list
|
||||||
|
---of strings, omitting the first result which is the complete match.
|
||||||
|
---@param offsets table<integer,integer>
|
||||||
|
---@param value string
|
||||||
|
---@return table<integer, string>
|
||||||
|
local function regex_result_to_table(offsets, value)
|
||||||
|
local result = {}
|
||||||
|
local offset_fix = 0
|
||||||
|
if not regex.find_offsets then
|
||||||
|
offset_fix = 1
|
||||||
|
end
|
||||||
|
for i=3, #offsets, 2 do
|
||||||
|
table.insert(result, value:sub(offsets[i], offsets[i+1]-offset_fix))
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get a matching config for the given filename or nil if nothing found.
|
||||||
|
---@param file_name string
|
||||||
|
---@param defaults? boolean Set indent size to defaults when needed,
|
||||||
|
---@return plugins.editorconfig.parser.section?
|
||||||
|
function Parser:getConfig(file_name, defaults)
|
||||||
|
if PLATFORM == "Windows" then
|
||||||
|
file_name = file_name:gsub("\\", "/")
|
||||||
|
end
|
||||||
|
|
||||||
|
local regex_match = regex.match
|
||||||
|
if regex.find_offsets then
|
||||||
|
regex_match = regex.find_offsets
|
||||||
|
end
|
||||||
|
|
||||||
|
local properties = {}
|
||||||
|
|
||||||
|
local found = false
|
||||||
|
for _, section in ipairs(self.sections) do
|
||||||
|
if section.rule.regex_compiled then
|
||||||
|
local negation = section.rule.negation
|
||||||
|
-- default rule
|
||||||
|
local matched = {regex_match(section.rule.regex_compiled, file_name)}
|
||||||
|
-- try equivalent rules if available
|
||||||
|
if not matched[1] and section.equivalent_rules then
|
||||||
|
for _, esection in ipairs(section.equivalent_rules) do
|
||||||
|
matched = {regex_match(esection.regex_compiled, file_name)}
|
||||||
|
if matched[1] then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if (matched[1] and not negation) or (not matched[1] and negation) then
|
||||||
|
local ranges_match = true
|
||||||
|
if section.rule.ranges then
|
||||||
|
local results = regex_result_to_table(matched, file_name)
|
||||||
|
if #results < #section.rule.ranges then
|
||||||
|
ranges_match = false
|
||||||
|
else
|
||||||
|
for i, range in ipairs(section.rule.ranges) do
|
||||||
|
local number = tonumber(results[i])
|
||||||
|
if not number then
|
||||||
|
ranges_match = false
|
||||||
|
break
|
||||||
|
end
|
||||||
|
if number < range[1] or number > range[2] then
|
||||||
|
ranges_match = false
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if ranges_match then
|
||||||
|
found = true
|
||||||
|
for name, value in pairs(section) do
|
||||||
|
if name ~= "rule" and name ~= "equivalent_rules" then
|
||||||
|
properties[name] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if found and defaults then
|
||||||
|
if properties.indent_style and properties.indent_style == "space" then
|
||||||
|
if properties.indent_size and not properties.tab_width then
|
||||||
|
properties.tab_width = 4
|
||||||
|
end
|
||||||
|
elseif properties.indent_style and properties.indent_style == "tab" then
|
||||||
|
if not properties.tab_width and not properties.indent_size then
|
||||||
|
properties.indent_size = "tab"
|
||||||
|
elseif properties.tab_width then
|
||||||
|
properties.indent_size = properties.tab_width
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return found and properties or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get a matching config for the given filename or nil if nothing found.
|
||||||
|
---@param file_name string
|
||||||
|
---@return string
|
||||||
|
function Parser:getConfigString(file_name)
|
||||||
|
local out = ""
|
||||||
|
local properties = self:getConfig(file_name, true)
|
||||||
|
if properties then
|
||||||
|
local config_sorted = {}
|
||||||
|
for name, value in pairs(properties) do
|
||||||
|
table.insert(config_sorted, {name = name, value = value})
|
||||||
|
end
|
||||||
|
table.sort(config_sorted, function(a, b)
|
||||||
|
return a.name < b.name
|
||||||
|
end)
|
||||||
|
for _, value in ipairs(config_sorted) do
|
||||||
|
out = out .. value.name .. "=" .. tostring(value.value) .. "\n"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
return Parser
|
||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
local core = require "core"
|
||||||
|
local tests = require "plugins.editorconfig.tests"
|
||||||
|
|
||||||
|
-- disable print buffer for immediate output
|
||||||
|
io.stdout:setvbuf "no"
|
||||||
|
|
||||||
|
-- overwrite to print into stdout
|
||||||
|
function core.error(format, ...)
|
||||||
|
print(string.format(format, ...))
|
||||||
|
end
|
||||||
|
|
||||||
|
function core.log(format, ...)
|
||||||
|
print(string.format(format, ...))
|
||||||
|
end
|
||||||
|
|
||||||
|
function core.log_quiet(format, ...)
|
||||||
|
print(string.format(format, ...))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- check if --parsers flag was given to only output the path expressions and
|
||||||
|
-- their conversion into regular expressions.
|
||||||
|
local PARSERS = false
|
||||||
|
for _, argument in ipairs(ARGS) do
|
||||||
|
if argument == "--parsers" then
|
||||||
|
PARSERS = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not PARSERS then
|
||||||
|
require "plugins.editorconfig.tests.glob"
|
||||||
|
require "plugins.editorconfig.tests.parser"
|
||||||
|
require "plugins.editorconfig.tests.properties"
|
||||||
|
|
||||||
|
tests.run()
|
||||||
|
else
|
||||||
|
-- Globs
|
||||||
|
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/glob/braces.in")
|
||||||
|
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/glob/brackets.in")
|
||||||
|
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/glob/question.in")
|
||||||
|
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/glob/star.in")
|
||||||
|
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/glob/star_star.in")
|
||||||
|
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/glob/utf8char.in")
|
||||||
|
|
||||||
|
-- Parser
|
||||||
|
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/parser/basic.in")
|
||||||
|
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/parser/bom.in")
|
||||||
|
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/parser/comments.in")
|
||||||
|
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/parser/comments_and_newlines.in")
|
||||||
|
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/parser/comments_only.in")
|
||||||
|
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/parser/crlf.in")
|
||||||
|
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/parser/empty.in")
|
||||||
|
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/parser/limits.in")
|
||||||
|
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/parser/newlines_only.in")
|
||||||
|
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/parser/whitespace.in")
|
||||||
|
|
||||||
|
-- Properties
|
||||||
|
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/properties/indent_size_default.in")
|
||||||
|
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/properties/lowercase_names.in")
|
||||||
|
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/properties/lowercase_values.in")
|
||||||
|
tests.add_parser(USERDIR .. "/plugins/editorconfig/tests/properties/tab_width_default.in")
|
||||||
|
|
||||||
|
tests.run_parsers()
|
||||||
|
end
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
; test { and }
|
||||||
|
|
||||||
|
root=true
|
||||||
|
|
||||||
|
; word choice
|
||||||
|
[*.{py,js,html}]
|
||||||
|
choice=true
|
||||||
|
|
||||||
|
; single choice
|
||||||
|
[{single}.b]
|
||||||
|
choice=single
|
||||||
|
|
||||||
|
; empty choice
|
||||||
|
[{}.c]
|
||||||
|
empty=all
|
||||||
|
|
||||||
|
; choice with empty word
|
||||||
|
[a{b,c,}.d]
|
||||||
|
empty=word
|
||||||
|
|
||||||
|
; choice with empty words
|
||||||
|
[a{,b,,c,}.e]
|
||||||
|
empty=words
|
||||||
|
|
||||||
|
; no closing brace
|
||||||
|
[{.f]
|
||||||
|
closing=false
|
||||||
|
|
||||||
|
; nested braces
|
||||||
|
[{word,{also},this}.g]
|
||||||
|
nested=true
|
||||||
|
|
||||||
|
; nested braces, adjacent at start
|
||||||
|
[{{a,b},c}.k]
|
||||||
|
nested_start=true
|
||||||
|
|
||||||
|
; nested braces, adjacent at end
|
||||||
|
[{a,{b,c}}.l]
|
||||||
|
nested_end=true
|
||||||
|
|
||||||
|
; closing inside beginning
|
||||||
|
[{},b}.h]
|
||||||
|
closing=inside
|
||||||
|
|
||||||
|
; opening inside beginning
|
||||||
|
[{{,b,c{d}.i]
|
||||||
|
unmatched=true
|
||||||
|
|
||||||
|
; escaped comma
|
||||||
|
[{a\,b,cd}.txt]
|
||||||
|
comma=yes
|
||||||
|
|
||||||
|
; escaped closing brace
|
||||||
|
[{e,\},f}.txt]
|
||||||
|
closing=yes
|
||||||
|
|
||||||
|
; escaped backslash
|
||||||
|
[{g,\\,i}.txt]
|
||||||
|
backslash=yes
|
||||||
|
|
||||||
|
; patterns nested in braces
|
||||||
|
[{some,a{*c,b}[ef]}.j]
|
||||||
|
patterns=nested
|
||||||
|
|
||||||
|
; numeric braces
|
||||||
|
[{3..120}]
|
||||||
|
number=true
|
||||||
|
|
||||||
|
; alphabetical
|
||||||
|
[{aardvark..antelope}]
|
||||||
|
words=a
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
; test [ and ]
|
||||||
|
|
||||||
|
root=true
|
||||||
|
|
||||||
|
; Character choice
|
||||||
|
[[ab].a]
|
||||||
|
choice=true
|
||||||
|
|
||||||
|
; Negative character choice
|
||||||
|
[[!ab].b]
|
||||||
|
choice=false
|
||||||
|
|
||||||
|
; Character range
|
||||||
|
[[d-g].c]
|
||||||
|
range=true
|
||||||
|
|
||||||
|
; Negative character range
|
||||||
|
[[!d-g].d]
|
||||||
|
range=false
|
||||||
|
|
||||||
|
; Range and choice
|
||||||
|
[[abd-g].e]
|
||||||
|
range_and_choice=true
|
||||||
|
|
||||||
|
; Choice with dash
|
||||||
|
[[-ab].f]
|
||||||
|
choice_with_dash=true
|
||||||
|
|
||||||
|
; Close bracket inside
|
||||||
|
[[\]ab].g]
|
||||||
|
close_inside=true
|
||||||
|
|
||||||
|
; Close bracket outside
|
||||||
|
[[ab]].g]
|
||||||
|
close_outside=true
|
||||||
|
|
||||||
|
; Negative close bracket inside
|
||||||
|
[[!\]ab].g]
|
||||||
|
close_inside=false
|
||||||
|
|
||||||
|
; Negative¬close bracket outside
|
||||||
|
[[!ab]].g]
|
||||||
|
close_outside=false
|
||||||
|
|
||||||
|
; Slash inside brackets
|
||||||
|
[ab[e/]cd.i]
|
||||||
|
slash_inside=true
|
||||||
|
|
||||||
|
; Slash after an half-open bracket
|
||||||
|
[ab[/c]
|
||||||
|
slash_half_open=true
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
local tests = require "plugins.editorconfig.tests"
|
||||||
|
|
||||||
|
-- Tests for *
|
||||||
|
|
||||||
|
-- matches a single characters
|
||||||
|
tests.add("star_single_ML", "glob/star.in", "ace.c", "key=value[ \t\n\r]+keyc=valuec[ \t\n\r]*")
|
||||||
|
|
||||||
|
-- matches zero characters
|
||||||
|
tests.add("star_zero_ML", "glob/star.in", "ae.c", "key=value[ \t\n\r]+keyc=valuec[ \t\n\r]*")
|
||||||
|
|
||||||
|
-- matches multiple characters
|
||||||
|
tests.add("star_multiple_ML", "glob/star.in", "abcde.c", "key=value[ \t\n\r]+keyc=valuec[ \t\n\r]*")
|
||||||
|
|
||||||
|
-- does not match path separator
|
||||||
|
tests.add("star_over_slash", "glob/star.in", "a/e.c", "^[ \t\n\r]*keyc=valuec[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- star after a slash
|
||||||
|
tests.add("star_after_slash_ML", "glob/star.in", "Bar/foo.txt", "keyb=valueb[ \t\n\r]+keyc=valuec[ \t\n\r]*")
|
||||||
|
|
||||||
|
-- star matches a dot file after slash
|
||||||
|
tests.add("star_matches_dot_file_after_slash_ML", "glob/star.in", "Bar/.editorconfig", "keyb=valueb[ \t\n\r]+keyc=valuec[ \t\n\r]*")
|
||||||
|
|
||||||
|
-- star matches a dot file
|
||||||
|
tests.add("star_matches_dot_file", "glob/star.in", ".editorconfig", "^keyc=valuec[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- Tests for ?
|
||||||
|
|
||||||
|
-- matches a single character
|
||||||
|
tests.add("question_single", "glob/question.in", "some.c", "^key=value[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- does not match zero characters
|
||||||
|
tests.add("question_zero", "glob/question.in", "som.c", "^[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- does not match multiple characters
|
||||||
|
tests.add("question_multiple", "glob/question.in", "something.c", "^[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- does not match slash
|
||||||
|
tests.add("question_slash", "glob/question.in", "som/.c", "^[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- Tests for [ and ]
|
||||||
|
|
||||||
|
-- close bracket inside
|
||||||
|
tests.add("brackets_close_inside", "glob/brackets.in", "].g", "^close_inside=true[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- close bracket outside
|
||||||
|
tests.add("brackets_close_outside", "glob/brackets.in", "b].g", "^close_outside=true[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- negative close bracket inside
|
||||||
|
tests.add("brackets_nclose_inside", "glob/brackets.in", "c.g", "^close_inside=false[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- negative close bracket outside
|
||||||
|
tests.add("brackets_nclose_outside", "glob/brackets.in", "c].g", "^close_outside=false[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- character choice
|
||||||
|
tests.add("brackets_choice", "glob/brackets.in", "a.a", "^choice=true[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- character choice 2
|
||||||
|
tests.add("brackets_choice2", "glob/brackets.in", "c.a", "^[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- negative character choice
|
||||||
|
tests.add("brackets_nchoice", "glob/brackets.in", "c.b", "^choice=false[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- negative character choice 2
|
||||||
|
tests.add("brackets_nchoice2", "glob/brackets.in", "a.b", "^[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- character range
|
||||||
|
tests.add("brackets_range", "glob/brackets.in", "f.c", "^range=true[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- character range 2
|
||||||
|
tests.add("brackets_range2", "glob/brackets.in", "h.c", "^[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- negative character range
|
||||||
|
tests.add("brackets_nrange", "glob/brackets.in", "h.d", "^range=false[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- negative character range 2
|
||||||
|
tests.add("brackets_nrange2", "glob/brackets.in", "f.d", "^[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- range and choice
|
||||||
|
tests.add("brackets_range_and_choice", "glob/brackets.in", "e.e",
|
||||||
|
"^range_and_choice=true[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- character choice with a dash
|
||||||
|
tests.add("brackets_choice_with_dash", "glob/brackets.in", "-.f",
|
||||||
|
"^choice_with_dash=true[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- slash inside brackets
|
||||||
|
tests.add("brackets_slash_inside1", "glob/brackets.in", "ab/cd.i",
|
||||||
|
"^[ \t\n\r]*$")
|
||||||
|
tests.add("brackets_slash_inside2", "glob/brackets.in", "abecd.i",
|
||||||
|
"^[ \t\n\r]*$")
|
||||||
|
tests.add("brackets_slash_inside3", "glob/brackets.in", "ab[e/]cd.i",
|
||||||
|
"^slash_inside=true[ \t\n\r]*$")
|
||||||
|
tests.add("brackets_slash_inside4", "glob/brackets.in", "ab[/c",
|
||||||
|
"^slash_half_open=true[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- Tests for { and }
|
||||||
|
|
||||||
|
-- word choice
|
||||||
|
tests.add("braces_word_choice1", "glob/braces.in", "test.py", "^choice=true[ \t\n\r]*$")
|
||||||
|
tests.add("braces_word_choice2", "glob/braces.in", "test.js", "^choice=true[ \t\n\r]*$")
|
||||||
|
tests.add("braces_word_choice3", "glob/braces.in", "test.html", "^choice=true[ \t\n\r]*$")
|
||||||
|
tests.add("braces_word_choice4", "glob/braces.in", "test.pyc", "^[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- single choice
|
||||||
|
tests.add("braces_single_choice", "glob/braces.in", "{single}.b", "^choice=single[ \t\n\r]*$")
|
||||||
|
tests.add("braces_single_choice_negative", "glob/braces.in", ".b", "^[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- empty choice
|
||||||
|
tests.add("braces_empty_choice", "glob/braces.in", "{}.c", "^empty=all[ \t\n\r]*$")
|
||||||
|
tests.add("braces_empty_choice_negative", "glob/braces.in", ".c", "^[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- choice with empty word
|
||||||
|
tests.add("braces_empty_word1", "glob/braces.in", "a.d", "^empty=word[ \t\n\r]*$")
|
||||||
|
tests.add("braces_empty_word2", "glob/braces.in", "ab.d", "^empty=word[ \t\n\r]*$")
|
||||||
|
tests.add("braces_empty_word3", "glob/braces.in", "ac.d", "^empty=word[ \t\n\r]*$")
|
||||||
|
tests.add("braces_empty_word4", "glob/braces.in", "a,.d", "^[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- choice with empty words
|
||||||
|
tests.add("braces_empty_words1", "glob/braces.in", "a.e", "^empty=words[ \t\n\r]*$")
|
||||||
|
tests.add("braces_empty_words2", "glob/braces.in", "ab.e", "^empty=words[ \t\n\r]*$")
|
||||||
|
tests.add("braces_empty_words3", "glob/braces.in", "ac.e", "^empty=words[ \t\n\r]*$")
|
||||||
|
tests.add("braces_empty_words4", "glob/braces.in", "a,.e", "^[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- no closing brace
|
||||||
|
tests.add("braces_no_closing", "glob/braces.in", "{.f", "^closing=false[ \t\n\r]*$")
|
||||||
|
tests.add("braces_no_closing_negative", "glob/braces.in", ".f", "^[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- nested braces
|
||||||
|
tests.add("braces_nested1", "glob/braces.in", "word,this}.g", "^[ \t\n\r]*$")
|
||||||
|
tests.add("braces_nested2", "glob/braces.in", "{also,this}.g", "^[ \t\n\r]*$")
|
||||||
|
tests.add("braces_nested3", "glob/braces.in", "word.g", "^nested=true[ \t\n\r]*$")
|
||||||
|
tests.add("braces_nested4", "glob/braces.in", "{also}.g", "^nested=true[ \t\n\r]*$")
|
||||||
|
tests.add("braces_nested5", "glob/braces.in", "this.g", "^nested=true[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- nested braces, adjacent at start
|
||||||
|
tests.add("braces_nested_start1", "glob/braces.in", "{{a,b},c}.k", "^[ \t\n\r]*$")
|
||||||
|
tests.add("braces_nested_start2", "glob/braces.in", "{a,b}.k", "^[ \t\n\r]*$")
|
||||||
|
tests.add("braces_nested_start3", "glob/braces.in", "a.k", "^nested_start=true[ \t\n\r]*$")
|
||||||
|
tests.add("braces_nested_start4", "glob/braces.in", "b.k", "^nested_start=true[ \t\n\r]*$")
|
||||||
|
tests.add("braces_nested_start5", "glob/braces.in", "c.k", "^nested_start=true[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- nested braces, adjacent at end
|
||||||
|
tests.add("braces_nested_end1", "glob/braces.in", "{a,{b,c}}.l", "^[ \t\n\r]*$")
|
||||||
|
tests.add("braces_nested_end2", "glob/braces.in", "{b,c}.l", "^[ \t\n\r]*$")
|
||||||
|
tests.add("braces_nested_end3", "glob/braces.in", "a.l", "^nested_end=true[ \t\n\r]*$")
|
||||||
|
tests.add("braces_nested_end4", "glob/braces.in", "b.l", "^nested_end=true[ \t\n\r]*$")
|
||||||
|
tests.add("braces_nested_end5", "glob/braces.in", "c.l", "^nested_end=true[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- closing inside beginning
|
||||||
|
tests.add("braces_closing_in_beginning", "glob/braces.in", "{},b}.h", "^closing=inside[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- missing closing braces
|
||||||
|
tests.add("braces_unmatched1", "glob/braces.in", "{{,b,c{d}.i", "^unmatched=true[ \t\n\r]*$")
|
||||||
|
tests.add("braces_unmatched2", "glob/braces.in", "{.i", "^[ \t\n\r]*$")
|
||||||
|
tests.add("braces_unmatched3", "glob/braces.in", "b.i", "^[ \t\n\r]*$")
|
||||||
|
tests.add("braces_unmatched4", "glob/braces.in", "c{d.i", "^[ \t\n\r]*$")
|
||||||
|
tests.add("braces_unmatched5", "glob/braces.in", ".i", "^[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- escaped comma
|
||||||
|
tests.add("braces_escaped_comma1", "glob/braces.in", "a,b.txt", "^comma=yes[ \t\n\r]*$")
|
||||||
|
tests.add("braces_escaped_comma2", "glob/braces.in", "a.txt", "^[ \t\n\r]*$")
|
||||||
|
tests.add("braces_escaped_comma3", "glob/braces.in", "cd.txt", "^comma=yes[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- escaped closing brace
|
||||||
|
tests.add("braces_escaped_brace1", "glob/braces.in", "e.txt", "^closing=yes[ \t\n\r]*$")
|
||||||
|
tests.add("braces_escaped_brace2", "glob/braces.in", "}.txt", "^closing=yes[ \t\n\r]*$")
|
||||||
|
tests.add("braces_escaped_brace3", "glob/braces.in", "f.txt", "^closing=yes[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- escaped backslash
|
||||||
|
tests.add("braces_escaped_backslash1", "glob/braces.in", "g.txt", "^backslash=yes[ \t\n\r]*$")
|
||||||
|
if PLATFORM ~= "Windows" then
|
||||||
|
tests.add("braces_escaped_backslash2", "glob/braces.in", "\\.txt", "^backslash=yes[ \t\n\r]*$")
|
||||||
|
end
|
||||||
|
tests.add("braces_escaped_backslash3", "glob/braces.in", "i.txt", "^backslash=yes[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- patterns nested in braces
|
||||||
|
tests.add("braces_patterns_nested1", "glob/braces.in", "some.j", "^patterns=nested[ \t\n\r]*$")
|
||||||
|
tests.add("braces_patterns_nested2", "glob/braces.in", "abe.j", "^patterns=nested[ \t\n\r]*$")
|
||||||
|
tests.add("braces_patterns_nested3", "glob/braces.in", "abf.j", "^patterns=nested[ \t\n\r]*$")
|
||||||
|
tests.add("braces_patterns_nested4", "glob/braces.in", "abg.j", "^[ \t\n\r]*$")
|
||||||
|
tests.add("braces_patterns_nested5", "glob/braces.in", "ace.j", "^patterns=nested[ \t\n\r]*$")
|
||||||
|
tests.add("braces_patterns_nested6", "glob/braces.in", "acf.j", "^patterns=nested[ \t\n\r]*$")
|
||||||
|
tests.add("braces_patterns_nested7", "glob/braces.in", "acg.j", "^[ \t\n\r]*$")
|
||||||
|
tests.add("braces_patterns_nested8", "glob/braces.in", "abce.j", "^patterns=nested[ \t\n\r]*$")
|
||||||
|
tests.add("braces_patterns_nested9", "glob/braces.in", "abcf.j", "^patterns=nested[ \t\n\r]*$")
|
||||||
|
tests.add("braces_patterns_nested10", "glob/braces.in", "abcg.j", "^[ \t\n\r]*$")
|
||||||
|
tests.add("braces_patterns_nested11", "glob/braces.in", "ae.j", "^[ \t\n\r]*$")
|
||||||
|
tests.add("braces_patterns_nested12", "glob/braces.in", ".j", "^[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- numeric brace range
|
||||||
|
tests.add("braces_numeric_range1", "glob/braces.in", "1", "^[ \t\n\r]*$")
|
||||||
|
tests.add("braces_numeric_range2", "glob/braces.in", "3", "^number=true[ \t\n\r]*$")
|
||||||
|
tests.add("braces_numeric_range3", "glob/braces.in", "15", "^number=true[ \t\n\r]*$")
|
||||||
|
tests.add("braces_numeric_range4", "glob/braces.in", "60", "^number=true[ \t\n\r]*$")
|
||||||
|
tests.add("braces_numeric_range5", "glob/braces.in", "5a", "^[ \t\n\r]*$")
|
||||||
|
tests.add("braces_numeric_range6", "glob/braces.in", "120", "^number=true[ \t\n\r]*$")
|
||||||
|
tests.add("braces_numeric_range7", "glob/braces.in", "121", "^[ \t\n\r]*$")
|
||||||
|
tests.add("braces_numeric_range8", "glob/braces.in", "060", "^[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- alphabetical brace range: letters should not be considered for ranges
|
||||||
|
tests.add("braces_alpha_range1", "glob/braces.in", "{aardvark..antelope}", "^words=a[ \t\n\r]*$")
|
||||||
|
tests.add("braces_alpha_range2", "glob/braces.in", "a", "^[ \t\n\r]*$")
|
||||||
|
tests.add("braces_alpha_range3", "glob/braces.in", "aardvark", "^[ \t\n\r]*$")
|
||||||
|
tests.add("braces_alpha_range4", "glob/braces.in", "agreement", "^[ \t\n\r]*$")
|
||||||
|
tests.add("braces_alpha_range5", "glob/braces.in", "antelope", "^[ \t\n\r]*$")
|
||||||
|
tests.add("braces_alpha_range6", "glob/braces.in", "antimatter", "^[ \t\n\r]*$")
|
||||||
|
|
||||||
|
|
||||||
|
-- Tests for **
|
||||||
|
|
||||||
|
-- test EditorConfig files with UTF-8 characters larger than 127
|
||||||
|
tests.add("utf_8_char", "glob/utf8char.in", "中文.txt", "^key=value[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- matches over path separator
|
||||||
|
tests.add("star_star_over_separator1", "glob/star_star.in", "a/z.c", "^key1=value1[ \t\n\r]*$")
|
||||||
|
tests.add("star_star_over_separator2", "glob/star_star.in", "amnz.c", "^key1=value1[ \t\n\r]*$")
|
||||||
|
tests.add("star_star_over_separator3", "glob/star_star.in", "am/nz.c", "^key1=value1[ \t\n\r]*$")
|
||||||
|
tests.add("star_star_over_separator4", "glob/star_star.in", "a/mnz.c", "^key1=value1[ \t\n\r]*$")
|
||||||
|
tests.add("star_star_over_separator5", "glob/star_star.in", "amn/z.c", "^key1=value1[ \t\n\r]*$")
|
||||||
|
tests.add("star_star_over_separator6", "glob/star_star.in", "a/mn/z.c", "^key1=value1[ \t\n\r]*$")
|
||||||
|
|
||||||
|
tests.add("star_star_over_separator7", "glob/star_star.in", "b/z.c", "^key2=value2[ \t\n\r]*$")
|
||||||
|
tests.add("star_star_over_separator8", "glob/star_star.in", "b/mnz.c", "^key2=value2[ \t\n\r]*$")
|
||||||
|
tests.add("star_star_over_separator9", "glob/star_star.in", "b/mn/z.c", "^key2=value2[ \t\n\r]*$")
|
||||||
|
tests.add("star_star_over_separator10", "glob/star_star.in", "bmnz.c", "^[ \t\n\r]*$")
|
||||||
|
tests.add("star_star_over_separator11", "glob/star_star.in", "bm/nz.c", "^[ \t\n\r]*$")
|
||||||
|
tests.add("star_star_over_separator12", "glob/star_star.in", "bmn/z.c", "^[ \t\n\r]*$")
|
||||||
|
|
||||||
|
tests.add("star_star_over_separator13", "glob/star_star.in", "c/z.c", "^key3=value3[ \t\n\r]*$")
|
||||||
|
tests.add("star_star_over_separator14", "glob/star_star.in", "cmn/z.c", "^key3=value3[ \t\n\r]*$")
|
||||||
|
tests.add("star_star_over_separator15", "glob/star_star.in", "c/mn/z.c", "^key3=value3[ \t\n\r]*$")
|
||||||
|
tests.add("star_star_over_separator16", "glob/star_star.in", "cmnz.c", "^[ \t\n\r]*$")
|
||||||
|
tests.add("star_star_over_separator17", "glob/star_star.in", "cm/nz.c", "^[ \t\n\r]*$")
|
||||||
|
tests.add("star_star_over_separator18", "glob/star_star.in", "c/mnz.c", "^[ \t\n\r]*$")
|
||||||
|
|
||||||
|
tests.add("star_star_over_separator19", "glob/star_star.in", "d/z.c", "^key4=value4[ \t\n\r]*$")
|
||||||
|
tests.add("star_star_over_separator20", "glob/star_star.in", "d/mn/z.c", "^key4=value4[ \t\n\r]*$")
|
||||||
|
tests.add("star_star_over_separator21", "glob/star_star.in", "dmnz.c", "^[ \t\n\r]*$")
|
||||||
|
tests.add("star_star_over_separator22", "glob/star_star.in", "dm/nz.c", "^[ \t\n\r]*$")
|
||||||
|
tests.add("star_star_over_separator23", "glob/star_star.in", "d/mnz.c", "^[ \t\n\r]*$")
|
||||||
|
tests.add("star_star_over_separator24", "glob/star_star.in", "dmn/z.c", "^[ \t\n\r]*$")
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
; test ?
|
||||||
|
|
||||||
|
root=true
|
||||||
|
|
||||||
|
[som?.c]
|
||||||
|
key=value
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
; test *
|
||||||
|
|
||||||
|
root=true
|
||||||
|
|
||||||
|
[a*e.c]
|
||||||
|
key=value
|
||||||
|
|
||||||
|
[Bar/*]
|
||||||
|
keyb=valueb
|
||||||
|
|
||||||
|
[*]
|
||||||
|
keyc=valuec
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
; test **
|
||||||
|
|
||||||
|
root=true
|
||||||
|
|
||||||
|
[a**z.c]
|
||||||
|
key1=value1
|
||||||
|
|
||||||
|
[b/**z.c]
|
||||||
|
key2=value2
|
||||||
|
|
||||||
|
[c**/z.c]
|
||||||
|
key3=value3
|
||||||
|
|
||||||
|
[d/**/z.c]
|
||||||
|
key4=value4
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
; test EditorConfig files with UTF-8 characters larger than 127
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[中文.txt]
|
||||||
|
key = value
|
||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
local Parser = require "plugins.editorconfig.parser"
|
||||||
|
|
||||||
|
local tests = {}
|
||||||
|
|
||||||
|
---@class tests.test
|
||||||
|
---@field name string Name of test
|
||||||
|
---@field config string Path to config file
|
||||||
|
---@field in_match string A path to test against the config
|
||||||
|
---@field out_match string A regex to match against the result
|
||||||
|
|
||||||
|
---Registered tests
|
||||||
|
---@type tests.test[]
|
||||||
|
tests.list = {}
|
||||||
|
|
||||||
|
--- parsers cache
|
||||||
|
---@type table<string,plugins.editorconfig.parser>
|
||||||
|
local parsers = {}
|
||||||
|
setmetatable(parsers, {
|
||||||
|
__index = function(t, k)
|
||||||
|
local v = rawget(t, k)
|
||||||
|
if v == nil then
|
||||||
|
v = Parser.new(k)
|
||||||
|
rawset(t, k, v)
|
||||||
|
end
|
||||||
|
return v
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
---Adds color to given text on non windows systems.
|
||||||
|
---@param text string
|
||||||
|
---@param color "red" | "green" | "yellow"
|
||||||
|
---@return string colorized_text
|
||||||
|
local function colorize(text, color)
|
||||||
|
if PLATFORM ~= "Windows" then
|
||||||
|
if color == "green" then
|
||||||
|
return "\27[92m"..text.."\27[0m"
|
||||||
|
elseif color == "red" then
|
||||||
|
return "\27[91m"..text.."\27[0m"
|
||||||
|
elseif color == "yellow" then
|
||||||
|
return "\27[93m"..text.."\27[0m"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return text
|
||||||
|
end
|
||||||
|
|
||||||
|
local PASSED = colorize("PASSED", "green")
|
||||||
|
local FAILED = colorize("FAILED", "red")
|
||||||
|
|
||||||
|
---Runs an individual test (executed by tests.run())
|
||||||
|
---@param name string Test name
|
||||||
|
---@param config_path string Relative path to tests diretory for a [config].in
|
||||||
|
---@param in_match string Filename to match
|
||||||
|
---@param out_match string | table Result to match regex
|
||||||
|
function tests.check_config(name, config_path, in_match, out_match, pos, total)
|
||||||
|
if type(out_match) == "string" then
|
||||||
|
out_match = { out_match }
|
||||||
|
end
|
||||||
|
local parser = parsers[USERDIR .. "/plugins/editorconfig/tests/" .. config_path]
|
||||||
|
local config = parser:getConfigString(in_match)
|
||||||
|
local passed = true
|
||||||
|
for _, match in ipairs(out_match) do
|
||||||
|
if not regex.match(match, config) then
|
||||||
|
passed = false
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if pos then
|
||||||
|
pos = "[" .. pos .. "/" .. total .. "] "
|
||||||
|
else
|
||||||
|
pos = ""
|
||||||
|
end
|
||||||
|
if passed then
|
||||||
|
print(pos .. string.format("%s - %s - '%s': %s", name, in_match, config_path, PASSED))
|
||||||
|
else
|
||||||
|
print(pos .. string.format("%s - %s - '%s': %s", name, in_match, config_path, FAILED))
|
||||||
|
print(config)
|
||||||
|
end
|
||||||
|
return passed
|
||||||
|
end
|
||||||
|
|
||||||
|
---Register a new test to be run later.
|
||||||
|
---@param name string Test name
|
||||||
|
---@param config_path string Relative path to tests diretory for a [config].in
|
||||||
|
---@param in_match string Filename to match
|
||||||
|
---@param out_match string | table Result to match regex
|
||||||
|
function tests.add(name, config_path, in_match, out_match)
|
||||||
|
table.insert(tests.list, {
|
||||||
|
name = name,
|
||||||
|
config = config_path,
|
||||||
|
in_match = in_match,
|
||||||
|
out_match = out_match
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
---Runs all registered tests and outputs the results to terminal.
|
||||||
|
function tests.run()
|
||||||
|
print "========================================================="
|
||||||
|
print "Running Tests"
|
||||||
|
print "========================================================="
|
||||||
|
local failed = 0
|
||||||
|
local passed = 0
|
||||||
|
local total = #tests.list
|
||||||
|
for i, test in ipairs(tests.list) do
|
||||||
|
local res = tests.check_config(
|
||||||
|
test.name, test.config, test.in_match, test.out_match, i, total
|
||||||
|
)
|
||||||
|
if res then passed = passed + 1 else failed = failed + 1 end
|
||||||
|
end
|
||||||
|
print "========================================================="
|
||||||
|
print (
|
||||||
|
string.format(
|
||||||
|
"%s %s %s",
|
||||||
|
colorize("Total tests: " .. #tests.list, "yellow"),
|
||||||
|
colorize("Passed: " .. passed, "green"),
|
||||||
|
colorize("Failed: " .. failed, "red")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print "========================================================="
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.add_parser(config_path)
|
||||||
|
return parsers[config_path]
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.run_parsers()
|
||||||
|
print "========================================================="
|
||||||
|
print "Running Parsers"
|
||||||
|
print "========================================================="
|
||||||
|
|
||||||
|
for config, parser in pairs(parsers) do
|
||||||
|
print "---------------------------------------------------------"
|
||||||
|
print(string.format("%s results:", config))
|
||||||
|
for _, section in ipairs(parser.sections) do
|
||||||
|
print(string.format("\nPath expression: %s", section.rule.expression))
|
||||||
|
print(string.format("Regex: %s", section.rule.regex))
|
||||||
|
print(string.format("Negation: %s", section.rule.negation and "true" or "false"))
|
||||||
|
print(string.format("Ranges: %s\n", section.rule.ranges and #section.rule.ranges or "0"))
|
||||||
|
end
|
||||||
|
print "---------------------------------------------------------"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return tests
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[*.a]
|
||||||
|
option1=value1
|
||||||
|
|
||||||
|
; repeat section
|
||||||
|
[*.a]
|
||||||
|
option2=value2
|
||||||
|
|
||||||
|
[*.b]
|
||||||
|
option1 = a
|
||||||
|
option2 = a
|
||||||
|
|
||||||
|
[b.b]
|
||||||
|
option2 = b
|
||||||
|
|
||||||
|
[*.b]
|
||||||
|
option1 = c
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
; test EditorConfig files with BOM
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
key = value
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
; test comments
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[test3.c]
|
||||||
|
; Comment before properties ignored
|
||||||
|
key=value
|
||||||
|
|
||||||
|
[test4.c]
|
||||||
|
key1=value1
|
||||||
|
; Comment between properties ignored
|
||||||
|
key2=value2
|
||||||
|
|
||||||
|
; Semicolon or hash at end of value read as part of value
|
||||||
|
[test5.c]
|
||||||
|
key1=value; not comment
|
||||||
|
key2=value # not comment
|
||||||
|
|
||||||
|
; Backslash before a semicolon or hash is part of the value
|
||||||
|
[test6.c]
|
||||||
|
key1=value \; not comment
|
||||||
|
key2=value \# not comment
|
||||||
|
|
||||||
|
; Escaped semicolon in section name
|
||||||
|
[test\;.c]
|
||||||
|
key=value
|
||||||
|
|
||||||
|
[test9.c]
|
||||||
|
# Comment before properties ignored
|
||||||
|
key=value
|
||||||
|
|
||||||
|
[test10.c]
|
||||||
|
key1=value1
|
||||||
|
# Comment between properties ignored
|
||||||
|
key2=value2
|
||||||
|
|
||||||
|
# Octothorpe at end of value read as part of value
|
||||||
|
[test11.c]
|
||||||
|
key=value# not comment
|
||||||
|
|
||||||
|
# Escaped octothorpe in value
|
||||||
|
[test12.c]
|
||||||
|
key=value \# not comment
|
||||||
|
|
||||||
|
# Escaped octothorpe in section name
|
||||||
|
[test\#.c]
|
||||||
|
key=value
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
# Just comments
|
||||||
|
|
||||||
|
# ... and newlines
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# Just a comment, nothing else
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
; test EditorConfig files with CRLF line separators
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
key = value
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
local tests = require "plugins.editorconfig.tests"
|
||||||
|
|
||||||
|
-- Basic parser tests
|
||||||
|
|
||||||
|
-- test repeat sections
|
||||||
|
tests.add("repeat_sections_ML", "parser/basic.in", "a.a", "option1=value1[ \t]*[\n\r]+option2=value2[ \t\n\r]*")
|
||||||
|
tests.add("basic_cascade_ML", "parser/basic.in", "b.b", "option1=c[ \t]*[\n\r]+option2=b[ \t\n\r]*")
|
||||||
|
|
||||||
|
-- Tests for whitespace parsing
|
||||||
|
|
||||||
|
-- test no whitespaces in property assignment
|
||||||
|
tests.add("no_whitespace", "parser/whitespace.in", "test1.c", "^key=value[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- test single spaces around equals sign
|
||||||
|
tests.add("single_spaces_around_equals", "parser/whitespace.in", "test2.c",
|
||||||
|
"^key=value[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- test multiple spaces around equals sign
|
||||||
|
tests.add("multiple_spaces_around_equals", "parser/whitespace.in", "test3.c",
|
||||||
|
"^key=value[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- test spaces before property name
|
||||||
|
tests.add("spaces_before_property_name", "parser/whitespace.in", "test4.c",
|
||||||
|
"^key=value[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- test spaces before after property value
|
||||||
|
tests.add("spaces_after_property_value", "parser/whitespace.in", "test5.c",
|
||||||
|
"^key=value[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- test blank lines between properties
|
||||||
|
tests.add("blank_lines_between_properties_ML", "parser/whitespace.in", "test6.c",
|
||||||
|
"key1=value1[ \t]*[\n\r]+key2=value2[ \t\n\r]*")
|
||||||
|
|
||||||
|
-- test spaces in section name
|
||||||
|
tests.add("spaces_in_section_name", "parser/whitespace.in", " test 7 ",
|
||||||
|
"^key=value[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- test spaces before section name are ignored
|
||||||
|
tests.add("spaces_before_section_name", "parser/whitespace.in", "test8.c",
|
||||||
|
"^key=value[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- test spaces after section name
|
||||||
|
tests.add("spaces_after_section_name", "parser/whitespace.in", "test9.c", "^key=value[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- test spaces at beginning of line between properties
|
||||||
|
tests.add("spaces_before_middle_property_ML", "parser/whitespace.in", "test10.c",
|
||||||
|
"key1=value1[ \t]*[\n\r]+key2=value2[ \t]*[\n\r]+key3=value3[ \t\n\r]*")
|
||||||
|
|
||||||
|
-- Tests for comment parsing
|
||||||
|
|
||||||
|
-- test comments ignored before properties
|
||||||
|
tests.add("comment_before_props", "parser/comments.in", "test3.c",
|
||||||
|
"^key=value[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- test comments ignored between properties
|
||||||
|
tests.add("comment_between_props_ML", "parser/comments.in", "test4.c",
|
||||||
|
"key1=value1[ \t]*[\n\r]+key2=value2[ \t\n\r]*")
|
||||||
|
|
||||||
|
-- test semicolons and hashes at end of property value are included in value
|
||||||
|
tests.add("semicolon_or_hash_in_property", "parser/comments.in", "test5.c",
|
||||||
|
"^key1=value; not comment[\n\r]+key2=value # not comment[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- test that backslashes before semicolons and hashes in property values
|
||||||
|
-- are included in value.
|
||||||
|
-- NOTE: [\\] matches a single literal backslash.
|
||||||
|
tests.add("backslashed_semicolon_or_hash_in_property", "parser/comments.in", "test6.c",
|
||||||
|
"^key1=value [\\\\]; not comment[\n\r]+key2=value [\\\\]# not comment[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- test escaped semicolons are included in section names
|
||||||
|
tests.add("escaped_semicolon_in_section", "parser/comments.in", "test;.c",
|
||||||
|
"^key=value[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- test octothorpe comments ignored before properties
|
||||||
|
tests.add("octothorpe_comment_before_props", "parser/comments.in", "test9.c",
|
||||||
|
"^key=value[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- test octothorpe comments ignored between properties
|
||||||
|
tests.add("octothorpe_comment_between_props_ML", "parser/comments.in", "test10.c",
|
||||||
|
"key1=value1[ \t]*[\n\r]+key2=value2[ \t\n\r]*")
|
||||||
|
|
||||||
|
-- test octothorpe at end of property value are included in value
|
||||||
|
tests.add("octothorpe_in_value", "parser/comments.in", "test11.c",
|
||||||
|
"^key=value# not comment[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- test escaped octothorpes are included in section names
|
||||||
|
tests.add("escaped_octothorpe_in_section", "parser/comments.in", "test#.c",
|
||||||
|
"^key=value[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- test EditorConfig files with BOM at the head
|
||||||
|
tests.add("bom_at_head", "parser/bom.in", "a.c", "^key=value[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- test EditorConfig files with CRLF line separators
|
||||||
|
tests.add("crlf_linesep", "parser/crlf.in", "a.c", "^key=value[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- Test minimum supported lengths of section name, key and value
|
||||||
|
tests.add("min_supported_key_length", "parser/limits.in", "test1",
|
||||||
|
"^aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=v1024[ \t\n\r]*$")
|
||||||
|
tests.add("min_supported_value_length", "parser/limits.in", "test2",
|
||||||
|
"^k4096=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[ \t\n\r]*$")
|
||||||
|
tests.add("min_supported_section_name_length", "parser/limits.in", "test3",
|
||||||
|
"^k1024=v1024[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- Empty .editorconfig files
|
||||||
|
tests.add("empty_editorconfig_file", "parser/empty.in", "test4", "^[ \t\n\r]*$")
|
||||||
|
tests.add("newlines_only_editorconfig_file", "parser/newlines_only.in", "test4", "^[ \t\n\r]*$")
|
||||||
|
tests.add("comments_only_editorconfig_file", "parser/comments_only.in", "test4", "^[ \t\n\r]*$")
|
||||||
|
tests.add("comments_and_newlines_editorconfig_file", "parser/comments_and_newlines.in", "test4", "^[ \t\n\r]*$")
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
; minimum supported key length of 1024 characters
|
||||||
|
[test1]
|
||||||
|
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=v1024
|
||||||
|
|
||||||
|
; minimum supported value length of 4096 characters
|
||||||
|
[test2]
|
||||||
|
k4096=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||||
|
|
||||||
|
; minimum supported section name length of 1024 characters (excluding [] brackets)
|
||||||
|
[{test3,aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}]
|
||||||
|
k1024=v1024
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
; test whitespace usage
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
; no whitespace
|
||||||
|
[test1.c]
|
||||||
|
key=value
|
||||||
|
|
||||||
|
; spaces around equals
|
||||||
|
[test2.c]
|
||||||
|
key = value
|
||||||
|
|
||||||
|
; lots of space after equals
|
||||||
|
[test3.c]
|
||||||
|
key = value
|
||||||
|
|
||||||
|
; spaces before property name
|
||||||
|
[test4.c]
|
||||||
|
key=value
|
||||||
|
|
||||||
|
; spaces after property value
|
||||||
|
[test5.c]
|
||||||
|
key=value
|
||||||
|
|
||||||
|
; blank lines between properties
|
||||||
|
[test6.c]
|
||||||
|
|
||||||
|
key1=value1
|
||||||
|
|
||||||
|
key2=value2
|
||||||
|
|
||||||
|
; spaces in section name
|
||||||
|
[ test 7 ]
|
||||||
|
key=value
|
||||||
|
|
||||||
|
; spaces before section name
|
||||||
|
[test8.c]
|
||||||
|
key=value
|
||||||
|
|
||||||
|
; spaces after section name
|
||||||
|
[test9.c]
|
||||||
|
key=value
|
||||||
|
|
||||||
|
; spacing before middle property
|
||||||
|
[test10.c]
|
||||||
|
key1=value1
|
||||||
|
key2=value2
|
||||||
|
key3=value3
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[test.c]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[test2.c]
|
||||||
|
indent_style = space
|
||||||
|
|
||||||
|
[test3.c]
|
||||||
|
indent_style = tab
|
||||||
|
tab_width = 2
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
local tests = require "plugins.editorconfig.tests"
|
||||||
|
|
||||||
|
-- test tab_width default
|
||||||
|
tests.add("tab_width_default_ML", "properties/tab_width_default.in", "test.c",
|
||||||
|
"indent_size=4[ \t]*[\n\r]+indent_style=space[ \t]*[\n\r]+tab_width=4[\t\n\r]*")
|
||||||
|
|
||||||
|
-- Tab_width should not be set to any value if indent_size is "tab" and
|
||||||
|
-- tab_width is not set
|
||||||
|
tests.add("tab_width_default_indent_size_tab_ML", "properties/tab_width_default.in",
|
||||||
|
"test2.c", "indent_size=tab[ \t]*[\n\r]+indent_style=tab[ \t\n\r]*")
|
||||||
|
|
||||||
|
-- Test indent_size default. When indent_style is "tab", indent_size defaults to
|
||||||
|
-- "tab".
|
||||||
|
tests.add("indent_size_default_ML", "properties/indent_size_default.in", "test.c",
|
||||||
|
"indent_size=tab[ \t]*[\n\r]+indent_style=tab[ \t\n\r]*")
|
||||||
|
|
||||||
|
-- Test indent_size default. When indent_style is "space", indent_size has no
|
||||||
|
-- default value.
|
||||||
|
tests.add("indent_size_default_space", "properties/indent_size_default.in", "test2.c",
|
||||||
|
"^indent_style=space[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- Test indent_size default. When indent_style is "tab" and tab_width is set,
|
||||||
|
-- indent_size should default to tab_width
|
||||||
|
tests.add("indent_size_default_with_tab_width_ML",
|
||||||
|
"properties/indent_size_default.in", "test3.c",
|
||||||
|
"indent_size=2[ \t]*[\n\r]+indent_style=tab[ \t]*[\n\r]+tab_width=2[ \t\n\r]*")
|
||||||
|
|
||||||
|
-- test that same property values are lowercased (v0.9.0 properties)
|
||||||
|
tests.add("lowercase_values1_ML", "properties/lowercase_values.in", "test1.c",
|
||||||
|
"end_of_line=crlf[ \t]*[\n\r]+indent_style=space[ \t\n\r]*")
|
||||||
|
|
||||||
|
-- test that same property values are lowercased (v0.9.0 properties)
|
||||||
|
tests.add("lowercase_values2_ML", "properties/lowercase_values.in", "test2.c",
|
||||||
|
"charset=utf-8[ \t]*[\n\r]+insert_final_newline=true[ \t]*[\n\r]+trim_trailing_whitespace=false[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- test that same property values are not lowercased
|
||||||
|
tests.add("lowercase_values3", "properties/lowercase_values.in", "test3.c",
|
||||||
|
"^test_property=TestValue[ \t\n\r]*$")
|
||||||
|
|
||||||
|
-- test that all property names are lowercased
|
||||||
|
tests.add("lowercase_names", "properties/lowercase_names.in", "test.c",
|
||||||
|
"^testproperty=testvalue[ \t\n\r]*$")
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
; test that property names are lowercased
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[test.c]
|
||||||
|
TestProperty = testvalue
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
; test property name lowercasing
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[test1.c]
|
||||||
|
indent_style = Space
|
||||||
|
end_of_line = CRLF
|
||||||
|
|
||||||
|
[test2.c]
|
||||||
|
insert_final_newline = TRUE
|
||||||
|
trim_trailing_whitespace = False
|
||||||
|
charset = UTF-8
|
||||||
|
|
||||||
|
[test3.c]
|
||||||
|
test_property = TestValue
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[test.c]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[test2.c]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = tab
|
||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
local core = require "core"
|
||||||
|
local command = require "core.command"
|
||||||
|
local RootView = require "core.rootview"
|
||||||
|
local DocView = require "core.docview"
|
||||||
|
local Doc = require "core.doc"
|
||||||
|
local TreeView = require "plugins.treeview"
|
||||||
|
|
||||||
|
local RootView_open_doc = RootView.open_doc
|
||||||
|
function RootView:open_doc(doc)
|
||||||
|
local docview = RootView_open_doc(self, doc)
|
||||||
|
-- The absence of the ephemeral flag means that before this moment in this
|
||||||
|
-- node this document was not exists
|
||||||
|
if docview.ephemeral == nil then
|
||||||
|
local node = self:get_active_node_default()
|
||||||
|
-- We assume that ephemeral tab is always the last one
|
||||||
|
-- But user can drag and drop tabs so full check is needed
|
||||||
|
for i, v in ipairs(node.views) do
|
||||||
|
if v.ephemeral then
|
||||||
|
node:close_view(self.root_node, v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
docview.ephemeral = true
|
||||||
|
end
|
||||||
|
return docview
|
||||||
|
end
|
||||||
|
|
||||||
|
local Doc_get_name = DocView.get_name
|
||||||
|
function DocView:get_name()
|
||||||
|
return self.doc and self.ephemeral and ("~ " .. Doc_get_name(self) .. " ~")
|
||||||
|
or Doc_get_name(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Any change to the document makes the tab normal
|
||||||
|
local Doc_on_text_change = Doc.on_text_change
|
||||||
|
function Doc:on_text_change(type)
|
||||||
|
core.active_view.ephemeral = false
|
||||||
|
Doc_on_text_change(self, type)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Double clicking in the TreeView makes the tab normal
|
||||||
|
local TreeView_on_mouse_pressed = TreeView.on_mouse_pressed
|
||||||
|
function TreeView:on_mouse_pressed(button, x, y, clicks)
|
||||||
|
local result = TreeView_on_mouse_pressed(self, button, x, y, clicks)
|
||||||
|
if (clicks > 1) and (core.active_view.doc ~= nil) then
|
||||||
|
core.active_view.ephemeral = false
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Double clicking on a tab makes it normal
|
||||||
|
local RootView_on_mouse_pressed = RootView.on_mouse_pressed
|
||||||
|
function RootView:on_mouse_pressed(button, x, y, clicks)
|
||||||
|
local result = RootView_on_mouse_pressed(self, button, x, y, clicks)
|
||||||
|
if clicks > 1 then
|
||||||
|
local node = self.root_node:get_child_overlapping_point(x, y)
|
||||||
|
local idx = node:get_tab_overlapping_point(x, y)
|
||||||
|
if idx then
|
||||||
|
node.views[idx].ephemeral = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Dragging a tab makes it normal
|
||||||
|
local RootView_on_mouse_released = RootView.on_mouse_released
|
||||||
|
function RootView:on_mouse_released(button, x, y, ...)
|
||||||
|
if self.dragged_node then
|
||||||
|
if button == "left" then
|
||||||
|
if self.dragged_node.dragging then
|
||||||
|
local view = self.dragged_node.node.views[self.dragged_node.idx]
|
||||||
|
view.ephemeral = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return RootView_on_mouse_released(self, button, x, y, ...)
|
||||||
|
end
|
||||||
Vendored
+118
@@ -0,0 +1,118 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
local subprocess = require "process"
|
||||||
|
|
||||||
|
local core = require "core"
|
||||||
|
local style = require "core.style"
|
||||||
|
local config = require "core.config"
|
||||||
|
local common = require "core.common"
|
||||||
|
|
||||||
|
config.plugins.fontconfig = common.merge({ prefix = "" }, config.plugins.fontconfig)
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Example config (put it in user module):
|
||||||
|
|
||||||
|
```
|
||||||
|
local fontconfig = require "plugins.fontconfig"
|
||||||
|
fontconfig.use {
|
||||||
|
font = { name = 'sans', size = 13 * SCALE },
|
||||||
|
code_font = { name = 'monospace', size = 13 * SCALE },
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
if you want the fonts to load instantaneously on startup,
|
||||||
|
you can try your luck on fontconfig.use_blocking. I won't be responsible for
|
||||||
|
the slow startup time.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local function resolve_font(spec)
|
||||||
|
local scan_rate = 1 / config.fps
|
||||||
|
local proc = subprocess.start({ config.plugins.fontconfig.prefix .. "fc-match", "-s", "-f", "%{file}\n", spec }, {
|
||||||
|
stdin = subprocess.REDIRECT_DISCARD,
|
||||||
|
stdout = subprocess.REDIRECT_PIPE,
|
||||||
|
stderr = subprocess.REDIRECT_STDOUT
|
||||||
|
})
|
||||||
|
local prev
|
||||||
|
local lines = {}
|
||||||
|
while true do
|
||||||
|
coroutine.yield(scan_rate)
|
||||||
|
local buf = proc:read_stdout()
|
||||||
|
if not buf then break end
|
||||||
|
|
||||||
|
local last_line_start = 1
|
||||||
|
for line, ln in string.gmatch(buf, "([^\n]-)\n()") do
|
||||||
|
last_line_start = ln
|
||||||
|
if prev then line = prev .. line end
|
||||||
|
table.insert(lines, line)
|
||||||
|
end
|
||||||
|
prev = last_line_start < #buf and string.sub(buf, last_line_start)
|
||||||
|
end
|
||||||
|
if prev then table.insert(lines, prev) end
|
||||||
|
|
||||||
|
if proc:returncode() ~= 0 or #lines < 1 then
|
||||||
|
error(string.format("Cannot find a font matching the given specs: %q", spec), 0)
|
||||||
|
end
|
||||||
|
return lines[1]
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
function M.load(font_name, font_size, font_opt)
|
||||||
|
local font_file = resolve_font(font_name)
|
||||||
|
return renderer.font.load(font_file, font_size, font_opt)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.load_blocking(font_name, font_size, font_opt)
|
||||||
|
local co = coroutine.create(function()
|
||||||
|
return M.load(font_name, font_size, font_opt)
|
||||||
|
end)
|
||||||
|
local result
|
||||||
|
while coroutine.status(co) ~= "dead" do
|
||||||
|
local ok, err = coroutine.resume(co)
|
||||||
|
if not ok then error(err) end
|
||||||
|
result = err
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.load_group_blocking(group, font_size, font_opt)
|
||||||
|
local fonts = {}
|
||||||
|
for i in ipairs(group) do
|
||||||
|
local co = coroutine.create(function()
|
||||||
|
return M.load(group[i], font_size, font_opt)
|
||||||
|
end)
|
||||||
|
local result
|
||||||
|
while coroutine.status(co) ~= "dead" do
|
||||||
|
local ok, err = coroutine.resume(co)
|
||||||
|
if not ok then error(err) end
|
||||||
|
result = err
|
||||||
|
end
|
||||||
|
table.insert(fonts, result)
|
||||||
|
end
|
||||||
|
|
||||||
|
return renderer.font.group(fonts)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.use(spec)
|
||||||
|
core.add_thread(function()
|
||||||
|
for key, value in pairs(spec) do
|
||||||
|
style[key] = M.load(value.name, value.size, value)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- there is basically no need for this, but for the sake of completeness
|
||||||
|
-- I'll leave this here
|
||||||
|
function M.use_blocking(spec)
|
||||||
|
for key, value in pairs(spec) do
|
||||||
|
local font
|
||||||
|
if value.group then
|
||||||
|
font = M.load_group_blocking(value.group, value.size, value)
|
||||||
|
else
|
||||||
|
font = M.load_blocking(value.name, value.size, value)
|
||||||
|
end
|
||||||
|
style[key] = font
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
Vendored
+51
@@ -0,0 +1,51 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
local core = require "core"
|
||||||
|
local command = require "core.command"
|
||||||
|
local common = require "core.common"
|
||||||
|
local config = require "core.config"
|
||||||
|
|
||||||
|
local function exec(cmd)
|
||||||
|
local proc = process.start(cmd)
|
||||||
|
while proc:running() do
|
||||||
|
coroutine.yield(0.1)
|
||||||
|
end
|
||||||
|
if proc:returncode() > 0 then
|
||||||
|
core.error("ERROR - command: " .. table.concat(cmd, " "))
|
||||||
|
end
|
||||||
|
return proc:read_stdout() or ""
|
||||||
|
end
|
||||||
|
|
||||||
|
local function git_find_files_and_open(commit)
|
||||||
|
local git_root = exec({"git", "rev-parse", "--show-toplevel"}):match("^%s*(.-)%s*$")
|
||||||
|
local file_list_str = exec({"git", "show", "--name-only", "--pretty=format:", commit})
|
||||||
|
|
||||||
|
for str in string.gmatch(file_list_str, "([^\n]+)") do
|
||||||
|
-- Nomalize path as Git for Windows uses / as Unix based systems
|
||||||
|
local filename = common.normalize_path(git_root .. PATHSEP .. str)
|
||||||
|
|
||||||
|
-- Only open files within the commit whose names do not match the configured ignore file patterns
|
||||||
|
if not common.match_pattern(common.basename(filename), config.ignore_files) then
|
||||||
|
core.root_view:open_doc(core.open_doc(filename))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- works in any context
|
||||||
|
command.add(nil, {
|
||||||
|
["gitopen:open-from-commit"] = function(dv)
|
||||||
|
core.command_view:enter("Which commit? (default=HEAD)", {
|
||||||
|
submit = function(commit)
|
||||||
|
if commit == nil or commit == "" then
|
||||||
|
commit = "HEAD"
|
||||||
|
end
|
||||||
|
-- open the files in the background, return immediately
|
||||||
|
core.add_thread(
|
||||||
|
function ()
|
||||||
|
git_find_files_and_open(commit)
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
Vendored
+678
@@ -0,0 +1,678 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
-- version: 0.3.0
|
||||||
|
-- Git panel for Lite XL: uncommitted changes, staging, commits, history.
|
||||||
|
-- Self-contained, needs only the `git` CLI in PATH.
|
||||||
|
-- Toggle with Ctrl+Shift+G, the git button in the treeview toolbar or the
|
||||||
|
-- branch name in the status bar. When the treeview plugin is present the
|
||||||
|
-- panel replaces the folder list in the left pane; otherwise it opens as
|
||||||
|
-- its own pane.
|
||||||
|
-- In the panel: click a file to open it, click a commit to view its patch.
|
||||||
|
-- Keys (panel focused): s stage · u unstage · a stage all · c commit · r refresh
|
||||||
|
|
||||||
|
local core = require "core"
|
||||||
|
local common = require "core.common"
|
||||||
|
local config = require "core.config"
|
||||||
|
local style = require "core.style"
|
||||||
|
local keymap = require "core.keymap"
|
||||||
|
local command = require "core.command"
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
local View = require "core.view"
|
||||||
|
local Doc = require "core.doc"
|
||||||
|
local StatusView = require "core.statusview"
|
||||||
|
|
||||||
|
config.plugins.gitpanel = common.merge({
|
||||||
|
history_length = 40,
|
||||||
|
}, config.plugins.gitpanel)
|
||||||
|
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Async git helpers (run inside core.add_thread coroutines)
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Run a command, reading incrementally so large outputs (git show) cannot
|
||||||
|
-- deadlock on the pipe buffer. Must be called from a coroutine.
|
||||||
|
local function exec(args)
|
||||||
|
local proc = process.start(args)
|
||||||
|
if not proc then return "" end
|
||||||
|
local chunks = {}
|
||||||
|
while proc:running() do
|
||||||
|
local chunk = proc:read_stdout(65536)
|
||||||
|
if chunk and #chunk > 0 then
|
||||||
|
chunks[#chunks + 1] = chunk
|
||||||
|
else
|
||||||
|
coroutine.yield(0.05)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
while true do
|
||||||
|
local chunk = proc:read_stdout(65536)
|
||||||
|
if chunk and #chunk > 0 then chunks[#chunks + 1] = chunk else break end
|
||||||
|
end
|
||||||
|
return table.concat(chunks)
|
||||||
|
end
|
||||||
|
|
||||||
|
local state = { branch = nil, changes = {}, history = {}, clean = false, loading = false }
|
||||||
|
|
||||||
|
local function parse_numstat(out, map)
|
||||||
|
for line in (out .. "\n"):gmatch("([^\n]*)\n") do
|
||||||
|
local ins, del, path = line:match("^(%d+)%s+(%d+)%s+(.+)$")
|
||||||
|
if path then
|
||||||
|
path = path:match("^.- ->%s*(.+)$") or path -- renames: keep new path
|
||||||
|
map[path] = { n = tonumber(ins) or 0, d = tonumber(del) or 0 }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function refresh(callback)
|
||||||
|
if state.loading then return end
|
||||||
|
state.loading = true
|
||||||
|
core.add_thread(function()
|
||||||
|
local branch, changes, history
|
||||||
|
-- pcall so a failed git run (spawn error, ...) can never leave
|
||||||
|
-- state.loading stuck and silently kill every later refresh
|
||||||
|
local ok, err = pcall(function()
|
||||||
|
branch, changes, history = nil, {}, {}
|
||||||
|
if system.get_file_info(core.project_dir .. PATHSEP .. ".git") then
|
||||||
|
local work, staged = {}, {}
|
||||||
|
parse_numstat(exec { "git", "diff", "--numstat" }, work)
|
||||||
|
parse_numstat(exec { "git", "diff", "--cached", "--numstat" }, staged)
|
||||||
|
|
||||||
|
local status = exec { "git", "status", "--porcelain=v1", "-b" }
|
||||||
|
local first = true
|
||||||
|
for line in (status .. "\n"):gmatch("([^\n]*)\n") do
|
||||||
|
if first and line:match("^## ") then
|
||||||
|
branch = line:sub(4):match("^(.-)%.%.%.") or line:sub(4):match("^(%S+)")
|
||||||
|
first = false
|
||||||
|
elseif line ~= "" then
|
||||||
|
local xy, path = line:match("^(..) (.+)$")
|
||||||
|
if xy then
|
||||||
|
path = path:match("^(.-)%s+->%s+(.+)$") or path
|
||||||
|
local x, y = xy:sub(1, 1), xy:sub(2, 2)
|
||||||
|
local nums = (x ~= " ") and staged[path] or work[path]
|
||||||
|
changes[#changes + 1] = {
|
||||||
|
xy = xy,
|
||||||
|
st = xy == "??" and "??" or (x ~= " " and x or y),
|
||||||
|
path = path,
|
||||||
|
staged = x ~= " " and x ~= "?",
|
||||||
|
untracked = xy == "??",
|
||||||
|
plus = nums and nums.n or nil,
|
||||||
|
minus = nums and nums.d or nil,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local log = exec { "git", "log", "-n", tostring(config.plugins.gitpanel.history_length),
|
||||||
|
"--pretty=format:%h %s" }
|
||||||
|
for line in (log .. "\n"):gmatch("([^\n]*)\n") do
|
||||||
|
local hash, subject = line:match("^(%S+) (.*)$")
|
||||||
|
if hash then history[#history + 1] = { hash = hash, subject = subject } end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
state.loading = false
|
||||||
|
if ok then
|
||||||
|
state.branch, state.changes, state.history = branch, changes, history
|
||||||
|
state.clean = branch ~= nil and #changes == 0
|
||||||
|
else
|
||||||
|
core.error("git-panel: refresh failed: %s", tostring(err))
|
||||||
|
end
|
||||||
|
if callback and ok then callback() end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Panel view
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function row_height()
|
||||||
|
return style.font:get_height() + common.round(4 * SCALE)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function status_color(st)
|
||||||
|
if st == "M" or st == "T" or st == "R" or st == "C" then
|
||||||
|
return style.gitstatus_modification or style.warn
|
||||||
|
end
|
||||||
|
if st == "A" or st == "??" then return style.gitstatus_addition or style.good end
|
||||||
|
if st == "D" or st == "U" then return style.gitstatus_deletion or style.error end
|
||||||
|
return style.text
|
||||||
|
end
|
||||||
|
|
||||||
|
local GitPanelView = View:extend()
|
||||||
|
|
||||||
|
local show_commit -- forward declaration: called from on_mouse_pressed below
|
||||||
|
|
||||||
|
-- Set once the treeview plugin is available (it loads after us); when nil
|
||||||
|
-- the panel falls back to opening as its own split pane.
|
||||||
|
local treeview = nil
|
||||||
|
|
||||||
|
function GitPanelView:new()
|
||||||
|
GitPanelView.super.new(self)
|
||||||
|
self.hover = nil -- { section = "changes"|"history", idx }
|
||||||
|
self.mouse = { x = 0, y = 0 }
|
||||||
|
self.changes_offset = 0
|
||||||
|
self.history_offset = 0
|
||||||
|
self.changes_rows = {}
|
||||||
|
self.history_rows = {}
|
||||||
|
self.changes_visible = 1
|
||||||
|
self.history_visible = 1
|
||||||
|
self.mid_y = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function GitPanelView:get_name()
|
||||||
|
return "Git"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- While embedded in the treeview's pane, the pane width follows our size.x
|
||||||
|
-- (the node is locked along x). Animate toward the treeview's target so
|
||||||
|
-- divider drags resize us, and the folder view inherits the final width.
|
||||||
|
function GitPanelView:set_target_size(axis, value)
|
||||||
|
if axis == "x" and treeview then
|
||||||
|
treeview:set_target_size(axis, value)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function GitPanelView:update()
|
||||||
|
if treeview then
|
||||||
|
self:move_towards(self.size, "x", treeview.target_size)
|
||||||
|
end
|
||||||
|
GitPanelView.super.update(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The panel is split in two independently scrolling sections: the changes list
|
||||||
|
-- on the top half and the history pinned to the bottom half.
|
||||||
|
function GitPanelView:build_changes_rows()
|
||||||
|
local rows = {}
|
||||||
|
for _, e in ipairs(state.changes) do rows[#rows + 1] = { type = "file", entry = e } end
|
||||||
|
if #rows == 0 then
|
||||||
|
rows[#rows + 1] = {
|
||||||
|
type = "empty",
|
||||||
|
label = state.loading and "refreshing..."
|
||||||
|
or (state.branch and "working tree clean" or "not a git repository"),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return rows
|
||||||
|
end
|
||||||
|
|
||||||
|
function GitPanelView:build_history_rows()
|
||||||
|
local rows = {}
|
||||||
|
for _, c in ipairs(state.history) do rows[#rows + 1] = { type = "commit", entry = c } end
|
||||||
|
if #rows == 0 then
|
||||||
|
rows[#rows + 1] = {
|
||||||
|
type = "empty",
|
||||||
|
label = state.loading and "refreshing..." or "no commits yet",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return rows
|
||||||
|
end
|
||||||
|
|
||||||
|
local function draw_row_hover(x, y, w, rh, hovered)
|
||||||
|
if hovered then
|
||||||
|
local c = { table.unpack(style.treeview_hover_bg or style.line_highlight) }
|
||||||
|
c[4] = 120
|
||||||
|
renderer.draw_rect(x, y, w, rh, c)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function draw_file_row(x, y, w, rh, e, pad)
|
||||||
|
common.draw_text(style.font, status_color(e.st), e.st, nil, x + pad, y, 0, rh)
|
||||||
|
local tx = x + pad + style.font:get_width(e.st) + common.round(6 * SCALE)
|
||||||
|
common.draw_text(style.font, style.treeview_text or style.text, e.path, nil, tx, y, 0, rh)
|
||||||
|
if e.plus or e.minus then
|
||||||
|
local label = string.format("+%d -%d", e.plus or 0, e.minus or 0)
|
||||||
|
common.draw_text(style.font, style.dim, label, nil,
|
||||||
|
x + w - style.font:get_width(label) - pad, y, 0, rh)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function draw_commit_row(x, y, rh, e, pad)
|
||||||
|
common.draw_text(style.font, style.dim, e.hash, nil, x + pad, y, 0, rh)
|
||||||
|
local tx = x + pad + style.font:get_width("0000000") + common.round(6 * SCALE)
|
||||||
|
common.draw_text(style.font, style.treeview_text or style.text, e.subject, nil, tx, y, 0, rh)
|
||||||
|
end
|
||||||
|
|
||||||
|
function GitPanelView:draw()
|
||||||
|
self:draw_background(style.background2)
|
||||||
|
local x, y = self.position.x, self.position.y
|
||||||
|
local w, h = self.size.x, self.size.y
|
||||||
|
local rh = row_height()
|
||||||
|
local pad = common.round(5 * SCALE)
|
||||||
|
local font_h = style.font:get_height()
|
||||||
|
|
||||||
|
-- history is pinned to the bottom half; snap the divider to a row boundary
|
||||||
|
local footer_h = font_h + common.round(4 * SCALE)
|
||||||
|
local half_rows = math.max(1, math.floor((h - footer_h) / 2 / rh))
|
||||||
|
local mid = y + half_rows * rh
|
||||||
|
self.mid_y = mid
|
||||||
|
|
||||||
|
self.changes_rows = self:build_changes_rows()
|
||||||
|
self.history_rows = self:build_history_rows()
|
||||||
|
self.changes_visible = math.max(1, half_rows - 2) -- branch + section headers
|
||||||
|
self.history_visible = math.max(1, half_rows - 1) -- section header
|
||||||
|
self.changes_offset = common.clamp(self.changes_offset,
|
||||||
|
0, math.max(0, #self.changes_rows - self.changes_visible))
|
||||||
|
self.history_offset = common.clamp(self.history_offset,
|
||||||
|
0, math.max(0, #self.history_rows - self.history_visible))
|
||||||
|
|
||||||
|
if state.branch then
|
||||||
|
common.draw_text(style.font, style.accent, "branch " .. state.branch, nil, x + pad, y, 0, rh)
|
||||||
|
else
|
||||||
|
common.draw_text(style.font, style.dim,
|
||||||
|
state.loading and "loading..." or "not a git repository", nil, x + pad, y, 0, rh)
|
||||||
|
end
|
||||||
|
|
||||||
|
common.draw_text(style.font, style.accent,
|
||||||
|
string.format("changes (%d)", #state.changes), nil, x + pad, y + rh, 0, rh)
|
||||||
|
local cy = y + 2 * rh
|
||||||
|
core.push_clip_rect(x, cy, w, mid - cy)
|
||||||
|
for i = self.changes_offset + 1,
|
||||||
|
math.min(#self.changes_rows, self.changes_offset + self.changes_visible) do
|
||||||
|
local row = self.changes_rows[i]
|
||||||
|
local ry = cy + (i - 1 - self.changes_offset) * rh
|
||||||
|
draw_row_hover(x, ry, w, rh,
|
||||||
|
self.hover and self.hover.section == "changes" and self.hover.idx == i)
|
||||||
|
if row.type == "file" then
|
||||||
|
draw_file_row(x, ry, w, rh, row.entry, pad)
|
||||||
|
else
|
||||||
|
common.draw_text(style.font, style.dim, row.label, nil, x + pad, ry, 0, rh)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
core.pop_clip_rect()
|
||||||
|
|
||||||
|
renderer.draw_rect(x, mid, w, SCALE, style.divider or style.dim)
|
||||||
|
|
||||||
|
common.draw_text(style.font, style.accent,
|
||||||
|
string.format("history (%d)", #state.history), nil, x + pad, mid, 0, rh)
|
||||||
|
local hy = mid + rh
|
||||||
|
core.push_clip_rect(x, hy, w, y + h - footer_h - hy)
|
||||||
|
for i = self.history_offset + 1,
|
||||||
|
math.min(#self.history_rows, self.history_offset + self.history_visible) do
|
||||||
|
local row = self.history_rows[i]
|
||||||
|
local ry = hy + (i - 1 - self.history_offset) * rh
|
||||||
|
draw_row_hover(x, ry, w, rh,
|
||||||
|
self.hover and self.hover.section == "history" and self.hover.idx == i)
|
||||||
|
if row.type == "commit" then
|
||||||
|
draw_commit_row(x, ry, rh, row.entry, pad)
|
||||||
|
else
|
||||||
|
common.draw_text(style.font, style.dim, row.label, nil, x + pad, ry, 0, rh)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
core.pop_clip_rect()
|
||||||
|
|
||||||
|
local hint = "s stage - u unstage - a all - c commit - r refresh"
|
||||||
|
common.draw_text(style.font, style.dim, hint, nil, x + pad,
|
||||||
|
y + h - font_h - common.round(2 * SCALE), 0, font_h)
|
||||||
|
end
|
||||||
|
|
||||||
|
function GitPanelView:on_mouse_moved(px, py, ...)
|
||||||
|
GitPanelView.super.on_mouse_moved(self, px, py, ...)
|
||||||
|
self.mouse.x, self.mouse.y = px, py
|
||||||
|
self.hover = nil
|
||||||
|
local rh = row_height()
|
||||||
|
local top_start = self.position.y + 2 * rh
|
||||||
|
local hist_start = self.mid_y + rh
|
||||||
|
if py >= top_start and py < self.mid_y then
|
||||||
|
local idx = math.floor((py - top_start) / rh) + self.changes_offset + 1
|
||||||
|
if self.changes_rows[idx] then self.hover = { section = "changes", idx = idx } end
|
||||||
|
elseif py >= hist_start then
|
||||||
|
local idx = math.floor((py - hist_start) / rh) + self.history_offset + 1
|
||||||
|
if self.history_rows[idx] then self.hover = { section = "history", idx = idx } end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function GitPanelView:on_mouse_pressed(button, px, py_, clicks)
|
||||||
|
GitPanelView.super.on_mouse_pressed(self, button, px, py_, clicks)
|
||||||
|
local hov = self.hover
|
||||||
|
if hov then
|
||||||
|
local row = hov.section == "changes"
|
||||||
|
and self.changes_rows[hov.idx] or self.history_rows[hov.idx]
|
||||||
|
if row and row.type == "file" and row.entry.st ~= "D" then
|
||||||
|
-- deleted files can't be opened
|
||||||
|
core.root_view:open_doc(core.open_doc(core.project_dir .. PATHSEP .. row.entry.path))
|
||||||
|
elseif row and row.type == "commit" then
|
||||||
|
show_commit(row.entry.hash, row.entry.subject)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function GitPanelView:on_mouse_left()
|
||||||
|
GitPanelView.super.on_mouse_left(self)
|
||||||
|
self.hover = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function GitPanelView:on_mouse_wheel(dy)
|
||||||
|
GitPanelView.super.on_mouse_wheel(self, dy)
|
||||||
|
local in_history = self.mouse.y >= self.mid_y
|
||||||
|
local rows_n = in_history and #self.history_rows or #self.changes_rows
|
||||||
|
local visible = in_history and self.history_visible or self.changes_visible
|
||||||
|
local offset = in_history and self.history_offset or self.changes_offset
|
||||||
|
offset = common.clamp(offset - dy * 3, 0, math.max(0, rows_n - visible))
|
||||||
|
if in_history then self.history_offset = offset else self.changes_offset = offset end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Actions
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local panel = nil -- the open GitPanelView instance, if any
|
||||||
|
|
||||||
|
local function open_scratch(name, text, syn)
|
||||||
|
local doc = Doc()
|
||||||
|
doc:reset()
|
||||||
|
local lines = {}
|
||||||
|
for line in (text .. "\n"):gmatch("([^\n]*)\n") do lines[#lines + 1] = line .. "\n" end
|
||||||
|
if #lines == 0 then lines[1] = "\n" end
|
||||||
|
doc.lines = lines
|
||||||
|
doc:set_filename(name, nil)
|
||||||
|
doc:reset_syntax()
|
||||||
|
if syn then doc.syntax = syn end -- else it stays whatever reset_syntax picked
|
||||||
|
core.root_view:open_doc(doc)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Commit diffs are highlighted with the official language_diff plugin when it
|
||||||
|
-- is installed (it registers its syntax under the name "Diff"); otherwise this
|
||||||
|
-- fallback covers `git show` output with the same colors.
|
||||||
|
style.syntax.diff_add = style.syntax.diff_add or { common.color "#72b886" }
|
||||||
|
style.syntax.diff_del = style.syntax.diff_del or { common.color "#f36161" }
|
||||||
|
|
||||||
|
local diff_syntax_fallback = syntax.add({
|
||||||
|
name = "Git Diff (gitpanel)",
|
||||||
|
patterns = {
|
||||||
|
{ pattern = "^commit %x+", type = "function" },
|
||||||
|
{ pattern = "^[%a%-]+: ", type = "keyword" },
|
||||||
|
{ pattern = "^diff %-%-git .*", type = "function" },
|
||||||
|
{ pattern = "^index .*", type = "comment" },
|
||||||
|
{ pattern = "^new .*", type = "comment" },
|
||||||
|
{ pattern = "^deleted .*", type = "comment" },
|
||||||
|
{ pattern = "^similarity index .*", type = "comment" },
|
||||||
|
{ pattern = "^rename .*", type = "comment" },
|
||||||
|
{ pattern = "^@@ .-@@", type = "number" },
|
||||||
|
{ pattern = "^%-%-%- .*", type = "keyword" },
|
||||||
|
{ pattern = "^%+%+%+ .*", type = "keyword" },
|
||||||
|
{ pattern = "^%-.*", type = "diff_del" },
|
||||||
|
{ pattern = "^%+.*", type = "diff_add" },
|
||||||
|
{ pattern = "^ %d+ files? changed.*", type = "function" },
|
||||||
|
},
|
||||||
|
symbols = {},
|
||||||
|
})
|
||||||
|
|
||||||
|
local function get_diff_syntax()
|
||||||
|
for _, s in ipairs(syntax.items) do
|
||||||
|
if s.name == "Diff" then return s end
|
||||||
|
end
|
||||||
|
return diff_syntax_fallback
|
||||||
|
end
|
||||||
|
|
||||||
|
function show_commit(hash, subject)
|
||||||
|
core.add_thread(function()
|
||||||
|
open_scratch("[git] " .. hash .. " " .. (subject or ""),
|
||||||
|
exec { "git", "show", "--no-color", hash }, get_diff_syntax())
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
local function hovered_file_entry()
|
||||||
|
if not (panel and panel.hover and panel.hover.section == "changes") then return nil end
|
||||||
|
local row = panel.changes_rows[panel.hover.idx]
|
||||||
|
return row and row.type == "file" and row.entry or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function run_on_entry(args_for)
|
||||||
|
local e = hovered_file_entry()
|
||||||
|
if not e or e.untracked and args_for == "unstage" then return end
|
||||||
|
local args
|
||||||
|
if args_for == "stage" then
|
||||||
|
args = { "git", "add", "--", e.path }
|
||||||
|
else
|
||||||
|
args = { "git", "reset", "-q", "HEAD", "--", e.path }
|
||||||
|
end
|
||||||
|
core.add_thread(function()
|
||||||
|
exec(args)
|
||||||
|
refresh()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function panel_active()
|
||||||
|
return panel ~= nil and core.active_view == panel
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Treeview integration: the panel replaces the folder list in the left pane,
|
||||||
|
-- toggled from the toolbar's git button. The treeview plugin loads after us,
|
||||||
|
-- so require it lazily. The treeview's leaf node is locked (no tab bar is
|
||||||
|
-- drawn there), which makes it a clean container to swap views in.
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function treeview_ready()
|
||||||
|
if treeview then return true end
|
||||||
|
if config.plugins.treeview == false then return false end
|
||||||
|
local ok, tv = pcall(require, "plugins.treeview")
|
||||||
|
if ok and tv and tv.node then
|
||||||
|
treeview = tv
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function treeview_leaf()
|
||||||
|
return treeview.node:get_node_for_view(treeview)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function pane_active()
|
||||||
|
if not (treeview and panel) then return false end
|
||||||
|
local leaf = treeview_leaf()
|
||||||
|
return leaf ~= nil and leaf.active_view == panel
|
||||||
|
end
|
||||||
|
|
||||||
|
local function pane_attach()
|
||||||
|
if not panel then panel = GitPanelView() end
|
||||||
|
local leaf = treeview_leaf()
|
||||||
|
if not leaf:get_view_idx(panel) then
|
||||||
|
panel.size.x = treeview.size.x -- else the locked pane would collapse on us
|
||||||
|
local locked = leaf.locked
|
||||||
|
leaf.locked = nil -- add_view() refuses locked nodes
|
||||||
|
leaf:add_view(panel) -- makes the panel the node's active view and focuses it
|
||||||
|
leaf.locked = locked
|
||||||
|
else
|
||||||
|
leaf:set_active_view(panel)
|
||||||
|
end
|
||||||
|
refresh()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function pane_detach()
|
||||||
|
local leaf = treeview_leaf()
|
||||||
|
local focused = core.active_view == panel
|
||||||
|
leaf.active_view = treeview -- route the pane back without stealing focus
|
||||||
|
leaf:remove_view(core.root_view.root_node, panel)
|
||||||
|
panel:on_mouse_left()
|
||||||
|
if focused then core.set_active_view(treeview) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function open_panel()
|
||||||
|
if panel and core.root_view:get_node_for_view(panel) then
|
||||||
|
refresh()
|
||||||
|
core.set_active_view(panel)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
panel = GitPanelView()
|
||||||
|
refresh()
|
||||||
|
local node = core.root_view:get_active_node()
|
||||||
|
node:split("left", panel)
|
||||||
|
core.set_active_view(panel)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function close_panel()
|
||||||
|
if not panel then return end
|
||||||
|
local node = core.root_view:get_node_for_view(panel)
|
||||||
|
if node then node:close_view(core.root_view.node, panel) end
|
||||||
|
panel = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function toggle_panel()
|
||||||
|
if treeview_ready() then
|
||||||
|
if pane_active() then pane_detach() else pane_attach() end
|
||||||
|
else
|
||||||
|
if panel_active() then close_panel() else open_panel() end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
command.add(nil, {
|
||||||
|
["git-panel:toggle"] = function() toggle_panel() end,
|
||||||
|
})
|
||||||
|
|
||||||
|
command.add(panel_active, {
|
||||||
|
["git-panel:stage"] = function()
|
||||||
|
if hovered_file_entry() then run_on_entry("stage") end
|
||||||
|
end,
|
||||||
|
["git-panel:unstage"] = function()
|
||||||
|
if hovered_file_entry() then run_on_entry("unstage") end
|
||||||
|
end,
|
||||||
|
["git-panel:stage-all"] = function()
|
||||||
|
core.add_thread(function()
|
||||||
|
exec { "git", "add", "-A" }
|
||||||
|
refresh()
|
||||||
|
end)
|
||||||
|
end,
|
||||||
|
["git-panel:refresh"] = function() refresh() end,
|
||||||
|
["git-panel:commit"] = function()
|
||||||
|
local staged = 0
|
||||||
|
for _, e in ipairs(state.changes) do if e.staged then staged = staged + 1 end end
|
||||||
|
if staged == 0 then
|
||||||
|
core.error("git-panel: nothing staged, press s on changed files first")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
core.command_view:enter("Commit message", function(text)
|
||||||
|
text = text:match("^%s*(.-)%s*$")
|
||||||
|
if text == "" then
|
||||||
|
core.error("git-panel: empty commit message")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
core.add_thread(function()
|
||||||
|
exec { "git", "commit", "-m", text }
|
||||||
|
refresh(function()
|
||||||
|
local left = 0
|
||||||
|
for _, e in ipairs(state.changes) do if e.staged then left = left + 1 end end
|
||||||
|
if left == 0 then
|
||||||
|
core.log("git-panel: committed: " .. text)
|
||||||
|
else
|
||||||
|
core.error("git-panel: commit failed, staged files remain")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
keymap.add {
|
||||||
|
["ctrl+shift+g"] = "git-panel:toggle",
|
||||||
|
}
|
||||||
|
|
||||||
|
keymap.add {
|
||||||
|
["s"] = "git-panel:stage",
|
||||||
|
["u"] = "git-panel:unstage",
|
||||||
|
["a"] = "git-panel:stage-all",
|
||||||
|
["c"] = "git-panel:commit",
|
||||||
|
["r"] = "git-panel:refresh",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Toolbar button: a git-branch icon next to the treeview toolbar buttons,
|
||||||
|
-- highlighted while the panel replaces the folder list. treeview loads after
|
||||||
|
-- us (plugins load alphabetically), so wait for it in a thread.
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- VS Code-style git-branch icon (vscode-codicons, MIT), rasterized offline to
|
||||||
|
-- opaque rect runs on a 34x34 grid; soft = partial-coverage edge cells
|
||||||
|
local GIT_ICON_GRID = 34
|
||||||
|
local GIT_ICON = {
|
||||||
|
{8,0,3,1},{6,1,7,1},{5,2,3,1},{11,2,3,1},{5,3,2,1},{12,3,2,1},{5,4,1,1},
|
||||||
|
{13,4,2,3},{4,5,2,1},{5,6,1,1},{5,7,2,1},{12,7,2,1},{21,7,6,1},{6,8,2,1},
|
||||||
|
{11,8,3,1},{20,8,3,1},{26,8,2,1},{7,9,6,1},{20,9,2,1},{27,9,2,1},{9,10,2,10},
|
||||||
|
{19,10,2,3},{28,10,1,1},{28,11,2,1},{28,12,1,1},{20,13,2,1},{27,13,2,1},
|
||||||
|
{20,14,3,1},{26,14,3,1},{21,15,7,1},{23,16,3,1},{23,17,2,1},{22,18,3,1},
|
||||||
|
{12,19,12,1},{9,20,14,1},{9,21,3,1},{9,22,2,2},{7,24,6,1},{6,25,2,1},
|
||||||
|
{11,25,3,1},{5,26,2,1},{12,26,2,1},{5,27,1,1},{13,27,2,3},{4,28,2,1},
|
||||||
|
{5,29,1,1},{5,30,2,1},{12,30,2,1},{5,31,3,1},{11,31,3,1},{6,32,7,1},{8,33,3,1}
|
||||||
|
}
|
||||||
|
local GIT_ICON_SOFT = {
|
||||||
|
{7,0,5,1},{5,1,9,1},{5,2,4,1},{10,2,4,1},{4,3,3,4},{12,3,3,5},{22,6,5,1},
|
||||||
|
{4,7,4,1},{21,7,7,1},{5,8,9,1},{20,8,9,1},{6,9,7,1},{19,9,3,5},{26,9,4,1},
|
||||||
|
{7,10,5,1},{27,10,3,4},{8,11,3,8},{20,14,4,1},{25,14,4,1},{20,15,9,1},
|
||||||
|
{22,16,5,1},{23,17,3,1},{21,18,4,1},{8,19,16,1},{8,20,15,1},{8,21,13,1},
|
||||||
|
{8,22,4,1},{7,23,5,1},{6,24,7,1},{5,25,9,1},{4,26,4,1},{12,26,3,5},
|
||||||
|
{4,27,3,4},{5,31,4,1},{10,31,4,1},{5,32,9,1},{7,33,5,1}
|
||||||
|
}
|
||||||
|
|
||||||
|
local function draw_git_icon(x, y, h, color)
|
||||||
|
local s = h / GIT_ICON_GRID
|
||||||
|
local soft = { table.unpack(color) }
|
||||||
|
soft[4] = (soft[4] or 255) * 0.5
|
||||||
|
local eps = s / 6 -- hide seams between adjacent runs
|
||||||
|
for _, r in ipairs(GIT_ICON_SOFT) do
|
||||||
|
renderer.draw_rect(x + r[1] * s, y + r[2] * s, r[3] * s + eps, r[4] * s + eps, soft)
|
||||||
|
end
|
||||||
|
for _, r in ipairs(GIT_ICON) do
|
||||||
|
renderer.draw_rect(x + r[1] * s, y + r[2] * s, r[3] * s + eps, r[4] * s + eps, color)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
core.add_thread(function()
|
||||||
|
for _ = 1, 20 do
|
||||||
|
if treeview_ready() then break end
|
||||||
|
coroutine.yield(1)
|
||||||
|
end
|
||||||
|
if not treeview then return end
|
||||||
|
local toolbar = treeview.toolbar
|
||||||
|
if not toolbar then return end -- toolbarview disabled: keybinding still works
|
||||||
|
-- blank symbol: the icon itself is painted by hand below
|
||||||
|
local git_button = { symbol = " ", command = "git-panel:toggle" }
|
||||||
|
table.insert(toolbar.toolbar_commands, git_button)
|
||||||
|
-- widen the pane if the default is too narrow for the extra button
|
||||||
|
treeview:set_target_size("x", math.max(treeview.target_size or 0, toolbar:get_min_width()))
|
||||||
|
-- dim by default, lit on hover, accent while the panel is showing
|
||||||
|
local toolbar_draw = toolbar.draw
|
||||||
|
function toolbar.draw(self)
|
||||||
|
toolbar_draw(self)
|
||||||
|
for item, x, y, w, h in self:each_item() do
|
||||||
|
if item == git_button then
|
||||||
|
local color = pane_active() and style.accent
|
||||||
|
or (self.hovered_item == item and style.text or style.dim)
|
||||||
|
draw_git_icon(x + (w - h) / 2, y, h, color)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Status bar "git button": make gitstatus's branch item open the panel.
|
||||||
|
-- gitstatus loads after us, so poll until its item exists.
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
core.add_thread(function()
|
||||||
|
for _ = 1, 20 do
|
||||||
|
local ok, item = pcall(core.status_view.get_item, core.status_view, "status:git")
|
||||||
|
if ok and item then
|
||||||
|
item.command = "git-panel:toggle"
|
||||||
|
item.tooltip = "git: click to open panel"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
coroutine.yield(1)
|
||||||
|
end
|
||||||
|
-- gitstatus absent: add our own fallback branch button
|
||||||
|
core.status_view:add_item({
|
||||||
|
name = "git-panel:branch",
|
||||||
|
alignment = StatusView.Item.RIGHT,
|
||||||
|
position = -2,
|
||||||
|
get_item = function()
|
||||||
|
if not state.branch then return {} end
|
||||||
|
return { style.accent, "branch: ", style.text, state.branch }
|
||||||
|
end,
|
||||||
|
on_click = function() command.perform("git-panel:toggle") end,
|
||||||
|
tooltip = "git: click to open panel",
|
||||||
|
separator = core.status_view.separator2,
|
||||||
|
})
|
||||||
|
end)
|
||||||
Vendored
+155
@@ -0,0 +1,155 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
local core = require "core"
|
||||||
|
local common = require "core.common"
|
||||||
|
local config = require "core.config"
|
||||||
|
local style = require "core.style"
|
||||||
|
local StatusView = require "core.statusview"
|
||||||
|
local TreeView = require "plugins.treeview"
|
||||||
|
|
||||||
|
config.plugins.gitstatus = common.merge({
|
||||||
|
color_icons = true,
|
||||||
|
recurse_submodules = true,
|
||||||
|
-- The config specification used by the settings gui
|
||||||
|
config_spec = {
|
||||||
|
name = "Git Status",
|
||||||
|
{
|
||||||
|
label = "Colorize icons",
|
||||||
|
description = "Colorize the icons as well",
|
||||||
|
path = "color_icons",
|
||||||
|
type = "toggle",
|
||||||
|
default = true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label = "Recurse Submodules",
|
||||||
|
description = "Also retrieve git stats from submodules.",
|
||||||
|
path = "recurse_submodules",
|
||||||
|
type = "toggle",
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, config.plugins.gitstatus)
|
||||||
|
|
||||||
|
style.gitstatus_addition = {common.color "#587c0c"}
|
||||||
|
style.gitstatus_modification = {common.color "#0c7d9d"}
|
||||||
|
style.gitstatus_deletion = {common.color "#94151b"}
|
||||||
|
|
||||||
|
local scan_rate = config.project_scan_rate or 5
|
||||||
|
local cached_color_for_item = {}
|
||||||
|
|
||||||
|
|
||||||
|
-- Override TreeView's get_item_text to add modification color
|
||||||
|
local treeview_get_item_text = TreeView.get_item_text
|
||||||
|
function TreeView:get_item_text(item, active, hovered)
|
||||||
|
local text, font, color = treeview_get_item_text(self, item, active, hovered)
|
||||||
|
if cached_color_for_item[item.abs_filename] then
|
||||||
|
color = cached_color_for_item[item.abs_filename]
|
||||||
|
end
|
||||||
|
return text, font, color
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Override TreeView's get_item_icon to add modification color
|
||||||
|
local treeview_get_item_icon = TreeView.get_item_icon
|
||||||
|
function TreeView:get_item_icon(item, active, hovered)
|
||||||
|
local character, font, color = treeview_get_item_icon(self, item, active, hovered)
|
||||||
|
if config.plugins.gitstatus and config.plugins.gitstatus.color_icons and cached_color_for_item[item.abs_filename] then
|
||||||
|
color = cached_color_for_item[item.abs_filename]
|
||||||
|
end
|
||||||
|
return character, font, color
|
||||||
|
end
|
||||||
|
|
||||||
|
local git = {
|
||||||
|
branch = nil,
|
||||||
|
inserts = 0,
|
||||||
|
deletes = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
local function exec(cmd)
|
||||||
|
local proc = process.start(cmd)
|
||||||
|
-- Don't use proc:wait() here - that will freeze the app.
|
||||||
|
-- Instead, rely on the fact that this is only called within
|
||||||
|
-- a coroutine, and yield for a fraction of a second, allowing
|
||||||
|
-- other stuff to happen while we wait for the process to complete.
|
||||||
|
while proc:running() do
|
||||||
|
coroutine.yield(0.1)
|
||||||
|
end
|
||||||
|
return proc:read_stdout() or ""
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
core.add_thread(function()
|
||||||
|
while true do
|
||||||
|
if system.get_file_info(".git") then
|
||||||
|
-- get branch name
|
||||||
|
git.branch = exec({"git", "rev-parse", "--abbrev-ref", "HEAD"}):match("[^\n]*")
|
||||||
|
|
||||||
|
local inserts = 0
|
||||||
|
local deletes = 0
|
||||||
|
|
||||||
|
-- get diff
|
||||||
|
local diff = exec({"git", "diff", "--numstat"})
|
||||||
|
if
|
||||||
|
config.plugins.gitstatus.recurse_submodules
|
||||||
|
and
|
||||||
|
system.get_file_info(".gitmodules")
|
||||||
|
then
|
||||||
|
local diff2 = exec({"git", "submodule", "foreach", "git diff --numstat"})
|
||||||
|
diff = diff .. diff2
|
||||||
|
end
|
||||||
|
|
||||||
|
-- forget the old state
|
||||||
|
cached_color_for_item = {}
|
||||||
|
|
||||||
|
local folder = core.project_dir
|
||||||
|
for line in string.gmatch(diff, "[^\n]+") do
|
||||||
|
local submodule = line:match("^Entering '(.+)'$")
|
||||||
|
if submodule then
|
||||||
|
folder = core.project_dir .. PATHSEP .. submodule
|
||||||
|
else
|
||||||
|
local ins, dels, path = line:match("(%d+)%s+(%d+)%s+(.+)")
|
||||||
|
if path then
|
||||||
|
inserts = inserts + (tonumber(ins) or 0)
|
||||||
|
deletes = deletes + (tonumber(dels) or 0)
|
||||||
|
local abs_path = folder .. PATHSEP .. path
|
||||||
|
-- Color this file, and each parent folder,
|
||||||
|
-- so you can see at a glance which folders
|
||||||
|
-- have modified files in them.
|
||||||
|
while abs_path do
|
||||||
|
cached_color_for_item[abs_path] = style.gitstatus_modification
|
||||||
|
abs_path = common.dirname(abs_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
git.inserts = inserts
|
||||||
|
git.deletes = deletes
|
||||||
|
|
||||||
|
else
|
||||||
|
git.branch = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
coroutine.yield(scan_rate)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
|
||||||
|
core.status_view:add_item({
|
||||||
|
name = "status:git",
|
||||||
|
alignment = StatusView.Item.RIGHT,
|
||||||
|
get_item = function()
|
||||||
|
if not git.branch then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
return {
|
||||||
|
(git.inserts ~= 0 or git.deletes ~= 0) and style.accent or style.text,
|
||||||
|
git.branch,
|
||||||
|
style.dim, " ",
|
||||||
|
git.inserts ~= 0 and style.accent or style.text, "+", git.inserts,
|
||||||
|
style.dim, " / ",
|
||||||
|
git.deletes ~= 0 and style.accent or style.text, "-", git.deletes,
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
position = -1,
|
||||||
|
tooltip = "branch and changes",
|
||||||
|
separator = core.status_view.separator2
|
||||||
|
})
|
||||||
Vendored
+39
@@ -0,0 +1,39 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
|
||||||
|
syntax.add{
|
||||||
|
name = "R",
|
||||||
|
files = {"%.r$", "%.rds$", "%.rda$", "%.rdata$", "%.R$"},
|
||||||
|
comment = "#",
|
||||||
|
patterns = {
|
||||||
|
{pattern = {"#", "\n"}, type = "comment"},
|
||||||
|
{pattern = {'"', '"'}, type = "string"},
|
||||||
|
{pattern = {"'", "'"}, type = "string"},
|
||||||
|
{pattern = "[%a_][%w_]*%f[(]", type = "function"},
|
||||||
|
{pattern = "[%a_][%w_]*", type = "symbol"},
|
||||||
|
{pattern = "[%+%-=/%*%^%%<>!|&]", type = "operator"},
|
||||||
|
{pattern = "0x[%da-fA-F]+", type = "number"},
|
||||||
|
{pattern = "-?%d+[%d%.eE]*", type = "number"},
|
||||||
|
{pattern = "-?%.?%d+", type = "number"},
|
||||||
|
},
|
||||||
|
symbols = {
|
||||||
|
["TRUE"] = "literal",
|
||||||
|
["FALSE"] = "literal",
|
||||||
|
["NA"] = "literal",
|
||||||
|
["NULL"] = "literal",
|
||||||
|
["Inf"] = "literal",
|
||||||
|
["if"] = "keyword",
|
||||||
|
["else"] = "keyword",
|
||||||
|
["while"] = "keyword",
|
||||||
|
["function"] = "keyword",
|
||||||
|
["break"] = "keyword",
|
||||||
|
["next"] = "keyword",
|
||||||
|
["repeat"] = "keyword",
|
||||||
|
["in"] = "keyword",
|
||||||
|
["for"] = "keyword",
|
||||||
|
["NA_integer"] = "keyword",
|
||||||
|
["NA_complex"] = "keyword",
|
||||||
|
["NA_character"] = "keyword",
|
||||||
|
["NA_real"] = "keyword"
|
||||||
|
}
|
||||||
|
}
|
||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
|
||||||
|
syntax.add {
|
||||||
|
name = "AngelScript",
|
||||||
|
files = { "%.as$", "%.asc$" },
|
||||||
|
comment = "//",
|
||||||
|
patterns = {
|
||||||
|
{ pattern = "//.-\n", type = "comment" },
|
||||||
|
{ pattern = { "/%*", "%*/" }, type = "comment" },
|
||||||
|
{ pattern = { "#", "[^\\]\n" }, type = "comment" },
|
||||||
|
{ pattern = { '"', '"', '\\' }, type = "string" },
|
||||||
|
{ pattern = { "'", "'", '\\' }, type = "string" },
|
||||||
|
{ pattern = "-?0[xX]%x+", type = "number" },
|
||||||
|
{ pattern = "-?0[bB][0-1]+", type = "number" },
|
||||||
|
{ pattern = "-?0[oO][0-7]+", type = "number" },
|
||||||
|
{ pattern = "-?%d+[%d%.eE]*f?", type = "number" },
|
||||||
|
{ pattern = "-?%.?%d+f?", type = "number" },
|
||||||
|
{ pattern = "&inout", type = "keyword" },
|
||||||
|
{ pattern = "&in", type = "keyword" },
|
||||||
|
{ pattern = "&out", type = "keyword" },
|
||||||
|
{ pattern = "[%a_][%w_]*@", type = "keyword2" },
|
||||||
|
{ pattern = "[%-%+!~@%?:&|%^<>%*/=%%]", type = "operator" },
|
||||||
|
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },
|
||||||
|
{ pattern = "[%a_][%w_]*", type = "symbol" },
|
||||||
|
},
|
||||||
|
symbols = {
|
||||||
|
-- Common
|
||||||
|
["shared"] = "keyword",
|
||||||
|
["external"] = "keyword",
|
||||||
|
["private"] = "keyword",
|
||||||
|
["protected"] = "keyword",
|
||||||
|
["const"] = "keyword",
|
||||||
|
["final"] = "keyword",
|
||||||
|
["abstract"] = "keyword",
|
||||||
|
["class"] = "keyword",
|
||||||
|
["typedef"] = "keyword",
|
||||||
|
["namespace"] = "keyword",
|
||||||
|
["interface"] = "keyword",
|
||||||
|
["import"] = "keyword",
|
||||||
|
["enum"] = "keyword",
|
||||||
|
["funcdef"] = "keyword",
|
||||||
|
["get"] = "keyword",
|
||||||
|
["set"] = "keyword",
|
||||||
|
["mixin"] = "keyword",
|
||||||
|
["void"] = "keyword2",
|
||||||
|
["int"] = "keyword2",
|
||||||
|
["int8"] = "keyword2",
|
||||||
|
["int16"] = "keyword2",
|
||||||
|
["int32"] = "keyword2",
|
||||||
|
["int64"] = "keyword2",
|
||||||
|
["uint"] = "keyword2",
|
||||||
|
["uint8"] = "keyword2",
|
||||||
|
["uint16"] = "keyword2",
|
||||||
|
["uint32"] = "keyword2",
|
||||||
|
["uint64"] = "keyword2",
|
||||||
|
["float"] = "keyword2",
|
||||||
|
["double"] = "keyword2",
|
||||||
|
["bool"] = "keyword2",
|
||||||
|
["auto"] = "keyword",
|
||||||
|
["override"] = "keyword",
|
||||||
|
["explicit"] = "keyword",
|
||||||
|
["property"] = "keyword",
|
||||||
|
["break"] = "keyword",
|
||||||
|
["continue"] = "keyword",
|
||||||
|
["return"] = "keyword",
|
||||||
|
["switch"] = "keyword",
|
||||||
|
["case"] = "keyword",
|
||||||
|
["default"] = "keyword",
|
||||||
|
["for"] = "keyword",
|
||||||
|
["while"] = "keyword",
|
||||||
|
["do"] = "keyword",
|
||||||
|
["if"] = "keyword",
|
||||||
|
["else"] = "keyword",
|
||||||
|
["try"] = "keyword",
|
||||||
|
["catch"] = "keyword",
|
||||||
|
["cast"] = "keyword",
|
||||||
|
["function"] = "keyword",
|
||||||
|
["true"] = "literal",
|
||||||
|
["false"] = "literal",
|
||||||
|
["null"] = "literal",
|
||||||
|
["is"] = "operator",
|
||||||
|
["and"] = "operator",
|
||||||
|
["or"] = "operator",
|
||||||
|
["xor"] = "operator",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
+538
@@ -0,0 +1,538 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
-- Support for RISC-V assembly
|
||||||
|
-- Note: kinda conflicts with x86 asm, must uninstall it or use force-syntax plugin
|
||||||
|
-- https://github.com/cheyao
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
|
||||||
|
syntax.add {
|
||||||
|
name = "RISC-V Assembly",
|
||||||
|
files = { "%.asm$", "%.[sS]$" },
|
||||||
|
comment = "#",
|
||||||
|
patterns = {
|
||||||
|
{ pattern = "#.*\n", type = "comment" },
|
||||||
|
{ pattern = { '"', '"', '\\' }, type = "string" },
|
||||||
|
{ pattern = { "'", "'", '\\' }, type = "string" },
|
||||||
|
{ pattern = "0[bB][0-1]+%W", type = "number" },
|
||||||
|
{ pattern = "0[xX]%x+", type = "number" },
|
||||||
|
{ pattern = "%%+[%a_][%w_]*", type = "function" },
|
||||||
|
{ pattern = "[%a%._][%w%._]*:%W", type = "function" },
|
||||||
|
{ pattern = "[^%p%a]%-?%d[%d%.]*", type = "number" },
|
||||||
|
{ pattern = "[%+%-=/%*%^%%<>!~|&%$]", type = "operator" },
|
||||||
|
{ pattern = "[%a_][%w_]*", type = "symbol" },
|
||||||
|
{ pattern = "%.%a+", type = "normal" }
|
||||||
|
},
|
||||||
|
symbols = {
|
||||||
|
-- Integer Registers
|
||||||
|
["x0"] = "literal",
|
||||||
|
["x1"] = "literal",
|
||||||
|
["x2"] = "literal",
|
||||||
|
["x3"] = "literal",
|
||||||
|
["x4"] = "literal",
|
||||||
|
["x5"] = "literal",
|
||||||
|
["x6"] = "literal",
|
||||||
|
["x7"] = "literal",
|
||||||
|
["x8"] = "literal",
|
||||||
|
["x9"] = "literal",
|
||||||
|
["x10"] = "literal",
|
||||||
|
["x11"] = "literal",
|
||||||
|
["x12"] = "literal",
|
||||||
|
["x13"] = "literal",
|
||||||
|
["x14"] = "literal",
|
||||||
|
["x15"] = "literal",
|
||||||
|
["x16"] = "literal",
|
||||||
|
["x17"] = "literal",
|
||||||
|
["x18"] = "literal",
|
||||||
|
["x19"] = "literal",
|
||||||
|
["x20"] = "literal",
|
||||||
|
["x21"] = "literal",
|
||||||
|
["x22"] = "literal",
|
||||||
|
["x23"] = "literal",
|
||||||
|
["x24"] = "literal",
|
||||||
|
["x25"] = "literal",
|
||||||
|
["x26"] = "literal",
|
||||||
|
["x27"] = "literal",
|
||||||
|
["x28"] = "literal",
|
||||||
|
["x29"] = "literal",
|
||||||
|
["x30"] = "literal",
|
||||||
|
["x31"] = "literal",
|
||||||
|
["zero"] = "literal",
|
||||||
|
["ra"] = "literal",
|
||||||
|
["sp"] = "literal",
|
||||||
|
["gp"] = "literal",
|
||||||
|
["tp"] = "literal",
|
||||||
|
["t0"] = "literal",
|
||||||
|
["t1"] = "literal",
|
||||||
|
["t2"] = "literal",
|
||||||
|
["fp"] = "literal",
|
||||||
|
["s0"] = "literal",
|
||||||
|
["s1"] = "literal",
|
||||||
|
["a0"] = "literal",
|
||||||
|
["a1"] = "literal",
|
||||||
|
["a2"] = "literal",
|
||||||
|
["a3"] = "literal",
|
||||||
|
["a4"] = "literal",
|
||||||
|
["a5"] = "literal",
|
||||||
|
["a6"] = "literal",
|
||||||
|
["a7"] = "literal",
|
||||||
|
["s2"] = "literal",
|
||||||
|
["s3"] = "literal",
|
||||||
|
["s4"] = "literal",
|
||||||
|
["s5"] = "literal",
|
||||||
|
["s6"] = "literal",
|
||||||
|
["s7"] = "literal",
|
||||||
|
["s8"] = "literal",
|
||||||
|
["s9"] = "literal",
|
||||||
|
["s10"] = "literal",
|
||||||
|
["s11"] = "literal",
|
||||||
|
["t3"] = "literal",
|
||||||
|
["t4"] = "literal",
|
||||||
|
["t5"] = "literal",
|
||||||
|
["t6"] = "literal",
|
||||||
|
["pc"] = "literal",
|
||||||
|
-- Floating-point Registers
|
||||||
|
["f0"] = "literal",
|
||||||
|
["f1"] = "literal",
|
||||||
|
["f2"] = "literal",
|
||||||
|
["f3"] = "literal",
|
||||||
|
["f4"] = "literal",
|
||||||
|
["f5"] = "literal",
|
||||||
|
["f6"] = "literal",
|
||||||
|
["f7"] = "literal",
|
||||||
|
["f8"] = "literal",
|
||||||
|
["f9"] = "literal",
|
||||||
|
["f10"] = "literal",
|
||||||
|
["f11"] = "literal",
|
||||||
|
["f12"] = "literal",
|
||||||
|
["f13"] = "literal",
|
||||||
|
["f14"] = "literal",
|
||||||
|
["f15"] = "literal",
|
||||||
|
["f16"] = "literal",
|
||||||
|
["f17"] = "literal",
|
||||||
|
["f18"] = "literal",
|
||||||
|
["f19"] = "literal",
|
||||||
|
["f20"] = "literal",
|
||||||
|
["f21"] = "literal",
|
||||||
|
["f22"] = "literal",
|
||||||
|
["f23"] = "literal",
|
||||||
|
["f24"] = "literal",
|
||||||
|
["f25"] = "literal",
|
||||||
|
["f26"] = "literal",
|
||||||
|
["f27"] = "literal",
|
||||||
|
["f28"] = "literal",
|
||||||
|
["f29"] = "literal",
|
||||||
|
["f30"] = "literal",
|
||||||
|
["f31"] = "literal",
|
||||||
|
|
||||||
|
["ft0"] = "literal",
|
||||||
|
["ft1"] = "literal",
|
||||||
|
["ft2"] = "literal",
|
||||||
|
["ft3"] = "literal",
|
||||||
|
["ft4"] = "literal",
|
||||||
|
["ft5"] = "literal",
|
||||||
|
["ft6"] = "literal",
|
||||||
|
["ft7"] = "literal",
|
||||||
|
|
||||||
|
["fs0"] = "literal",
|
||||||
|
["fs1"] = "literal",
|
||||||
|
|
||||||
|
["fa0"] = "literal",
|
||||||
|
["fa1"] = "literal",
|
||||||
|
["fa2"] = "literal",
|
||||||
|
["fa3"] = "literal",
|
||||||
|
["fa4"] = "literal",
|
||||||
|
["fa5"] = "literal",
|
||||||
|
["fa6"] = "literal",
|
||||||
|
["fa7"] = "literal",
|
||||||
|
|
||||||
|
["fa2"] = "literal",
|
||||||
|
["fa3"] = "literal",
|
||||||
|
["fa4"] = "literal",
|
||||||
|
["fa5"] = "literal",
|
||||||
|
["fa6"] = "literal",
|
||||||
|
["fa7"] = "literal",
|
||||||
|
["fa8"] = "literal",
|
||||||
|
["fa9"] = "literal",
|
||||||
|
["fa10"] = "literal",
|
||||||
|
["fa11"] = "literal",
|
||||||
|
|
||||||
|
["ft8"] = "literal",
|
||||||
|
["ft9"] = "literal",
|
||||||
|
["ft10"] = "literal",
|
||||||
|
["ft11"] = "literal",
|
||||||
|
|
||||||
|
-- Vector Registers
|
||||||
|
["v0"] = "literal",
|
||||||
|
["v1"] = "literal",
|
||||||
|
["v2"] = "literal",
|
||||||
|
["v3"] = "literal",
|
||||||
|
["v4"] = "literal",
|
||||||
|
["v5"] = "literal",
|
||||||
|
["v6"] = "literal",
|
||||||
|
["v7"] = "literal",
|
||||||
|
["v8"] = "literal",
|
||||||
|
["v9"] = "literal",
|
||||||
|
["v10"] = "literal",
|
||||||
|
["v11"] = "literal",
|
||||||
|
["v12"] = "literal",
|
||||||
|
["v13"] = "literal",
|
||||||
|
["v14"] = "literal",
|
||||||
|
["v15"] = "literal",
|
||||||
|
["v16"] = "literal",
|
||||||
|
["v17"] = "literal",
|
||||||
|
["v18"] = "literal",
|
||||||
|
["v19"] = "literal",
|
||||||
|
["v20"] = "literal",
|
||||||
|
["v21"] = "literal",
|
||||||
|
["v22"] = "literal",
|
||||||
|
["v23"] = "literal",
|
||||||
|
["v24"] = "literal",
|
||||||
|
["v25"] = "literal",
|
||||||
|
["v26"] = "literal",
|
||||||
|
["v27"] = "literal",
|
||||||
|
["v28"] = "literal",
|
||||||
|
["v29"] = "literal",
|
||||||
|
["v30"] = "literal",
|
||||||
|
["v31"] = "literal",
|
||||||
|
|
||||||
|
["vl"] = "literal",
|
||||||
|
["vtype"] = "literal",
|
||||||
|
["vzrm"] = "literal",
|
||||||
|
["vxsat"] = "literal",
|
||||||
|
|
||||||
|
-- RV32I instructions
|
||||||
|
["lui"] = "keyword",
|
||||||
|
["auipc"] = "keyword",
|
||||||
|
["jal"] = "keyword",
|
||||||
|
["jalr"] = "keyword",
|
||||||
|
["beq"] = "keyword",
|
||||||
|
["bne"] = "keyword",
|
||||||
|
["blt"] = "keyword",
|
||||||
|
["bge"] = "keyword",
|
||||||
|
["bltu"] = "keyword",
|
||||||
|
["bgeu"] = "keyword",
|
||||||
|
["lb"] = "keyword",
|
||||||
|
["lh"] = "keyword",
|
||||||
|
["lw"] = "keyword",
|
||||||
|
["lbu"] = "keyword",
|
||||||
|
["lhu"] = "keyword",
|
||||||
|
["sb"] = "keyword",
|
||||||
|
["sh"] = "keyword",
|
||||||
|
["sw"] = "keyword",
|
||||||
|
["addi"] = "keyword",
|
||||||
|
["slti"] = "keyword",
|
||||||
|
["sltiu"] = "keyword",
|
||||||
|
["xori"] = "keyword",
|
||||||
|
["ori"] = "keyword",
|
||||||
|
["andi"] = "keyword",
|
||||||
|
["slli"] = "keyword",
|
||||||
|
["srli"] = "keyword",
|
||||||
|
["srai"] = "keyword",
|
||||||
|
["add"] = "keyword",
|
||||||
|
["sub"] = "keyword",
|
||||||
|
["sll"] = "keyword",
|
||||||
|
["slt"] = "keyword",
|
||||||
|
["sltu"] = "keyword",
|
||||||
|
["xor"] = "keyword",
|
||||||
|
["srl"] = "keyword",
|
||||||
|
["sra"] = "keyword",
|
||||||
|
["or"] = "keyword",
|
||||||
|
["and"] = "keyword",
|
||||||
|
["fence"] = "keyword",
|
||||||
|
["fence.tso"] = "keyword",
|
||||||
|
["pause"] = "keyword",
|
||||||
|
["ecall"] = "keyword",
|
||||||
|
["ebreak"] = "keyword",
|
||||||
|
|
||||||
|
-- RV64I instructions
|
||||||
|
["lwu"] = "keyword",
|
||||||
|
["ld"] = "keyword",
|
||||||
|
["sd"] = "keyword",
|
||||||
|
["slli"] = "keyword",
|
||||||
|
["srli"] = "keyword",
|
||||||
|
["srai"] = "keyword",
|
||||||
|
["addiw"] = "keyword",
|
||||||
|
["slliw"] = "keyword",
|
||||||
|
["srliw"] = "keyword",
|
||||||
|
["sraiw"] = "keyword",
|
||||||
|
["addw"] = "keyword",
|
||||||
|
["subw"] = "keyword",
|
||||||
|
["sllw"] = "keyword",
|
||||||
|
["srlw"] = "keyword",
|
||||||
|
["sraw"] = "keyword",
|
||||||
|
|
||||||
|
-- Zifencei instructions
|
||||||
|
["fence.i"] = "keyword",
|
||||||
|
|
||||||
|
-- Zicsr instructions
|
||||||
|
["csrrw"] = "keyword",
|
||||||
|
["csrrs"] = "keyword",
|
||||||
|
["csrrc"] = "keyword",
|
||||||
|
["csrrwi"] = "keyword",
|
||||||
|
["csrrsi"] = "keyword",
|
||||||
|
["csrrci"] = "keyword",
|
||||||
|
|
||||||
|
-- RV32M instructions
|
||||||
|
["mul"] = "keyword",
|
||||||
|
["mulh"] = "keyword",
|
||||||
|
["mulhsu"] = "keyword",
|
||||||
|
["mulhu"] = "keyword",
|
||||||
|
["div"] = "keyword",
|
||||||
|
["divu"] = "keyword",
|
||||||
|
["rem"] = "keyword",
|
||||||
|
["remu"] = "keyword",
|
||||||
|
|
||||||
|
-- RV64M instructions
|
||||||
|
["mulw"] = "keyword",
|
||||||
|
["divw"] = "keyword",
|
||||||
|
["divuw"] = "keyword",
|
||||||
|
["remw"] = "keyword",
|
||||||
|
["remuw"] = "keyword",
|
||||||
|
|
||||||
|
-- RV32A instructions
|
||||||
|
["lr.w"] = "keyword",
|
||||||
|
["sc.w"] = "keyword",
|
||||||
|
["amoswap.w"] = "keyword",
|
||||||
|
["amoadd.w"] = "keyword",
|
||||||
|
["amoxor.w"] = "keyword",
|
||||||
|
["amoand.w"] = "keyword",
|
||||||
|
["amoor.w"] = "keyword",
|
||||||
|
["amomin.w"] = "keyword",
|
||||||
|
["amomax.w"] = "keyword",
|
||||||
|
["amominu.w"] = "keyword",
|
||||||
|
["amomaxu.w"] = "keyword",
|
||||||
|
|
||||||
|
-- RV64A instructions
|
||||||
|
["lr.d"] = "keyword",
|
||||||
|
["sc.d"] = "keyword",
|
||||||
|
["amoswap.d"] = "keyword",
|
||||||
|
["amoadd.d"] = "keyword",
|
||||||
|
["amoxor.d"] = "keyword",
|
||||||
|
["amoand.d"] = "keyword",
|
||||||
|
["amoor.d"] = "keyword",
|
||||||
|
["amomin.d"] = "keyword",
|
||||||
|
["amomax.d"] = "keyword",
|
||||||
|
["amominu.d"] = "keyword",
|
||||||
|
["amomaxu.d"] = "keyword",
|
||||||
|
|
||||||
|
-- RV32F instructions
|
||||||
|
["flw"] = "keyword",
|
||||||
|
["fsw"] = "keyword",
|
||||||
|
["fmadd.s"] = "keyword",
|
||||||
|
["fmsub.s"] = "keyword",
|
||||||
|
["fnmsub.s"] = "keyword",
|
||||||
|
["fnmadd.s"] = "keyword",
|
||||||
|
["fadd.s"] = "keyword",
|
||||||
|
["fsub.s"] = "keyword",
|
||||||
|
["fmul.s"] = "keyword",
|
||||||
|
["fdiv.s"] = "keyword",
|
||||||
|
["fsqrt.s"] = "keyword",
|
||||||
|
["fsgnj.s"] = "keyword",
|
||||||
|
["fsgnjn.s"] = "keyword",
|
||||||
|
["fsgnjx.s"] = "keyword",
|
||||||
|
["fmin.s"] = "keyword",
|
||||||
|
["fmax.s"] = "keyword",
|
||||||
|
["fcvt.w.s"] = "keyword",
|
||||||
|
["fcvt.wu.s"] = "keyword",
|
||||||
|
["fmv.x.w"] = "keyword",
|
||||||
|
["feq.s"] = "keyword",
|
||||||
|
["flt.s"] = "keyword",
|
||||||
|
["fle.s"] = "keyword",
|
||||||
|
["fclass.s"] = "keyword",
|
||||||
|
["fcvt.s.w"] = "keyword",
|
||||||
|
["fcvt.s.wu"] = "keyword",
|
||||||
|
["fmv.w.x"] = "keyword",
|
||||||
|
|
||||||
|
-- RV64F instructions
|
||||||
|
["fcvt.l.s"] = "keyword",
|
||||||
|
["fcvt.lu.s"] = "keyword",
|
||||||
|
["fcvt.s.l"] = "keyword",
|
||||||
|
["fcvt.s.lu"] = "keyword",
|
||||||
|
|
||||||
|
-- RV32D instructions
|
||||||
|
["fld"] = "keyword",
|
||||||
|
["fsd"] = "keyword",
|
||||||
|
["fmadd.d"] = "keyword",
|
||||||
|
["fmsub.d"] = "keyword",
|
||||||
|
["fnmsub.d"] = "keyword",
|
||||||
|
["fnmadd.d"] = "keyword",
|
||||||
|
["fadd.d"] = "keyword",
|
||||||
|
["fsub.d"] = "keyword",
|
||||||
|
["fmul.d"] = "keyword",
|
||||||
|
["fdiv.d"] = "keyword",
|
||||||
|
["fsqrt.d"] = "keyword",
|
||||||
|
["fsgnj.d"] = "keyword",
|
||||||
|
["fsgnjn.d"] = "keyword",
|
||||||
|
["fsgnjx.d"] = "keyword",
|
||||||
|
["fmin.d"] = "keyword",
|
||||||
|
["fmax.d"] = "keyword",
|
||||||
|
["fcvt.s.d"] = "keyword",
|
||||||
|
["fcvt.d.s"] = "keyword",
|
||||||
|
["feq.d"] = "keyword",
|
||||||
|
["flt.d"] = "keyword",
|
||||||
|
["fle.d"] = "keyword",
|
||||||
|
["fclass.d"] = "keyword",
|
||||||
|
["fcvt.w.d"] = "keyword",
|
||||||
|
["fcvt.wu.d"] = "keyword",
|
||||||
|
["fcvt.d.w"] = "keyword",
|
||||||
|
["fcvt.d.wu"] = "keyword",
|
||||||
|
|
||||||
|
-- RV64D instructions
|
||||||
|
["fcvt.l.d"] = "keyword",
|
||||||
|
["fcvt.lu.d"] = "keyword",
|
||||||
|
["fmv.x.d"] = "keyword",
|
||||||
|
["fcvt.d.l"] = "keyword",
|
||||||
|
["fcvt.d.lu"] = "keyword",
|
||||||
|
["fmv.d.x"] = "keyword",
|
||||||
|
|
||||||
|
-- RV32Q instructions
|
||||||
|
["flq"] = "keyword",
|
||||||
|
["fsq"] = "keyword",
|
||||||
|
["fmadd.q"] = "keyword",
|
||||||
|
["fmsub.q"] = "keyword",
|
||||||
|
["fnmsub.q"] = "keyword",
|
||||||
|
["fnmadd.q"] = "keyword",
|
||||||
|
["fadd.q"] = "keyword",
|
||||||
|
["fsub.q"] = "keyword",
|
||||||
|
["fmul.q"] = "keyword",
|
||||||
|
["fdiv.q"] = "keyword",
|
||||||
|
["fsqrt.q"] = "keyword",
|
||||||
|
["fsgnj.q"] = "keyword",
|
||||||
|
["fsgnjn.q"] = "keyword",
|
||||||
|
["fsgnjx.q"] = "keyword",
|
||||||
|
["fmin.q"] = "keyword",
|
||||||
|
["fmax.q"] = "keyword",
|
||||||
|
["fcvt.s.q"] = "keyword",
|
||||||
|
["fcvt.q.s"] = "keyword",
|
||||||
|
["fcvt.d.q"] = "keyword",
|
||||||
|
["fcvt.q.d"] = "keyword",
|
||||||
|
["feq.q"] = "keyword",
|
||||||
|
["flt.q"] = "keyword",
|
||||||
|
["fle.q"] = "keyword",
|
||||||
|
["fclass.q"] = "keyword",
|
||||||
|
["fcvt.w.q"] = "keyword",
|
||||||
|
["fcvt.wu.q"] = "keyword",
|
||||||
|
["fcvt.q.w"] = "keyword",
|
||||||
|
["fcvt.q.wu"] = "keyword",
|
||||||
|
|
||||||
|
-- RV64Q instructions
|
||||||
|
["fcvt.l.q"] = "keyword",
|
||||||
|
["fcvt.lu.q"] = "keyword",
|
||||||
|
["fcvt.q.l"] = "keyword",
|
||||||
|
["fcvt.q.lu"] = "keyword",
|
||||||
|
|
||||||
|
-- RV32Zfh instructions
|
||||||
|
["flh"] = "keyword",
|
||||||
|
["fsh"] = "keyword",
|
||||||
|
["fmadd.h"] = "keyword",
|
||||||
|
["fmsub.h"] = "keyword",
|
||||||
|
["fnmsub.h"] = "keyword",
|
||||||
|
["fnmadd.h"] = "keyword",
|
||||||
|
["fadd.h"] = "keyword",
|
||||||
|
["fsub.h"] = "keyword",
|
||||||
|
["fmul.h"] = "keyword",
|
||||||
|
["fdiv.h"] = "keyword",
|
||||||
|
["fsqrt.h"] = "keyword",
|
||||||
|
["fsgnj.h"] = "keyword",
|
||||||
|
["fsgnjn.h"] = "keyword",
|
||||||
|
["fsgnjx.h"] = "keyword",
|
||||||
|
["fmin.h"] = "keyword",
|
||||||
|
["fmax.h"] = "keyword",
|
||||||
|
["fcvt.s.h"] = "keyword",
|
||||||
|
["fcvt.h.s"] = "keyword",
|
||||||
|
["fcvt.d.h"] = "keyword",
|
||||||
|
["fcvt.h.d"] = "keyword",
|
||||||
|
["fcvt.q.h"] = "keyword",
|
||||||
|
["fcvt.h.q"] = "keyword",
|
||||||
|
["feq.h"] = "keyword",
|
||||||
|
["flt.h"] = "keyword",
|
||||||
|
["fle.h"] = "keyword",
|
||||||
|
["fclass.h"] = "keyword",
|
||||||
|
["fcvt.w.h"] = "keyword",
|
||||||
|
["fcvt.wu.h"] = "keyword",
|
||||||
|
["fmv.x.h"] = "keyword",
|
||||||
|
["fcvt.h.w"] = "keyword",
|
||||||
|
["fcvt.h.wu"] = "keyword",
|
||||||
|
["fmv.h.x"] = "keyword",
|
||||||
|
|
||||||
|
-- RV64Zfh instructions
|
||||||
|
["fcvt.l.h"] = "keyword",
|
||||||
|
["fcvt.lu.h"] = "keyword",
|
||||||
|
["fcvt.h.l"] = "keyword",
|
||||||
|
["fcvt.h.lu"] = "keyword",
|
||||||
|
|
||||||
|
-- Pesudo-instructions
|
||||||
|
["nop"] = "keyword",
|
||||||
|
["li"] = "keyword",
|
||||||
|
["mv"] = "keyword",
|
||||||
|
["not"] = "keyword",
|
||||||
|
["neg"] = "keyword",
|
||||||
|
["negw"] = "keyword",
|
||||||
|
["sext.w"] = "keyword",
|
||||||
|
["seqz"] = "keyword",
|
||||||
|
["snez"] = "keyword",
|
||||||
|
["sltz"] = "keyword",
|
||||||
|
["sgtz"] = "keyword",
|
||||||
|
["fmv.s"] = "keyword",
|
||||||
|
["fabs.s"] = "keyword",
|
||||||
|
["fneg.s"] = "keyword",
|
||||||
|
["fmv.d"] = "keyword",
|
||||||
|
["fabs.d"] = "keyword",
|
||||||
|
["fneg.d"] = "keyword",
|
||||||
|
["beqz"] = "keyword",
|
||||||
|
["bnez"] = "keyword",
|
||||||
|
["blez"] = "keyword",
|
||||||
|
["bgez"] = "keyword",
|
||||||
|
["bltz"] = "keyword",
|
||||||
|
["bgtz"] = "keyword",
|
||||||
|
["bgt"] = "keyword",
|
||||||
|
["ble"] = "keyword",
|
||||||
|
["bgtu"] = "keyword",
|
||||||
|
["bleu"] = "keyword",
|
||||||
|
["j"] = "keyword",
|
||||||
|
["jr"] = "keyword",
|
||||||
|
["ret"] = "keyword",
|
||||||
|
["call"] = "keyword",
|
||||||
|
["tail"] = "keyword",
|
||||||
|
|
||||||
|
-- Other
|
||||||
|
[".2byte"] = "keyword2",
|
||||||
|
[".4byte"] = "keyword2",
|
||||||
|
[".8byte"] = "keyword2",
|
||||||
|
[".half"] = "keyword2",
|
||||||
|
[".word"] = "keyword2",
|
||||||
|
[".dword"] = "keyword2",
|
||||||
|
[".byte"] = "keyword2",
|
||||||
|
[".dtpreldword"] = "keyword2",
|
||||||
|
[".dtprelword"] = "keyword2",
|
||||||
|
[".sleb128"] = "keyword2",
|
||||||
|
[".uleb128"] = "keyword2",
|
||||||
|
[".asciz"] = "keyword2",
|
||||||
|
[".string"] = "keyword2",
|
||||||
|
[".incbin"] = "keyword2",
|
||||||
|
[".zero"] = "keyword2",
|
||||||
|
|
||||||
|
[".align"] = "keyword2",
|
||||||
|
[".balign"] = "keyword2",
|
||||||
|
[".p2align"] = "keyword2",
|
||||||
|
|
||||||
|
[".globl"] = "keyword2",
|
||||||
|
[".local"] = "keyword2",
|
||||||
|
[".equ"] = "keyword2",
|
||||||
|
|
||||||
|
[".text"] = "keyword2",
|
||||||
|
[".data"] = "keyword2",
|
||||||
|
[".rodata"] = "keyword2",
|
||||||
|
[".bss"] = "keyword2",
|
||||||
|
[".comm"] = "keyword2",
|
||||||
|
[".common"] = "keyword2",
|
||||||
|
[".section"] = "keyword2",
|
||||||
|
|
||||||
|
[".option"] = "keyword2",
|
||||||
|
[".macro"] = "keyword2",
|
||||||
|
[".endm"] = "keyword2",
|
||||||
|
[".file"] = "keyword2",
|
||||||
|
[".ident"] = "keyword2",
|
||||||
|
[".size"] = "keyword2",
|
||||||
|
[".type"] = "keyword2",
|
||||||
|
},
|
||||||
|
}
|
||||||
+2215
File diff suppressed because it is too large
Load Diff
+155
@@ -0,0 +1,155 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
|
||||||
|
-- AHK has case insensitive grammer
|
||||||
|
local variables = {
|
||||||
|
"A_AhkPath", "A_AhkVersion", "A_AppData", "A_AppDataCommon", "A_AutoTrim",
|
||||||
|
"A_BatchLines", "A_CaretX", "A_CaretY", "A_Computername", "A_ControlDelay",
|
||||||
|
"A_Cursor", "A_DD", "A_DDD", "A_DDDD", "A_DefaultMouseSpeed",
|
||||||
|
"A_Desktop", "A_Desktopcommon", "A_Detecthiddentext", "A_Detecthiddenwindows", "A_Endchar",
|
||||||
|
"A_EventInfo", "A_ExitReason", "A_FileEncoding", "A_FormatFloat", "A_FormatInteger",
|
||||||
|
"A_Gui", "A_GuiControl", "A_GuiControlEvent", "A_GuiEvent", "A_GuiHeight",
|
||||||
|
"A_GuiWidth", "A_GuiX", "A_GuiY", "A_Hour", "A_IconFile",
|
||||||
|
"A_IconHidden", "A_IconNumber", "A_IconTip", "A_Index", "A_IpAddress1",
|
||||||
|
"A_IpAddress2", "A_IpAddress3", "A_IpAddress4", "A_Is64bitOS", "A_IsAdmin",
|
||||||
|
"A_IsCompiled", "A_IsCritical", "A_IsPaused", "A_IsSuspended", "A_IsUnicode",
|
||||||
|
"A_KeyDelay", "A_Language", "A_LastError", "A_LineFile", "A_LineNumber",
|
||||||
|
"A_LoopField", "A_LoopFileAttrib", "A_LoopFileDir", "A_LoopFileExt", "A_LoopFileFullPath",
|
||||||
|
"A_LoopFileLongPath", "A_LoopFileName", "A_LoopFileShortName", "A_LoopFileShortPath", "A_LoopFileSize",
|
||||||
|
"A_LoopFileSizeKB", "A_LoopFileSizeMB", "A_LoopFileTimeAccessed", "A_LoopFileTimeCreated", "A_LoopFileTimeModified",
|
||||||
|
"A_LoopReadLine", "A_LoopRegKey", "A_LoopRegName", "A_LoopRegSubKey", "A_LoopRegTimeModified",
|
||||||
|
"A_LoopRegType", "A_MDay", "A_Min", "A_MM", "A_MMM",
|
||||||
|
"A_MMMM", "A_Mon", "A_MouseDelay", "A_MSec", "A_MyDocuments",
|
||||||
|
"A_Now", "A_NowUTC", "A_NumBatchLines", "A_OSType", "A_OSVersion",
|
||||||
|
"A_PriorHotkey", "A_PriorKey", "A_ProgramFiles", "A_Programs", "A_ProgramsCommon",
|
||||||
|
"A_PtrSize", "A_RegView", "A_ScreenDpi", "A_ScreenHeight", "A_ScreenWidth",
|
||||||
|
"A_ScriptDir", "A_ScriptFullPath", "A_ScriptHwnd", "A_ScriptName", "A_Sec",
|
||||||
|
"A_Space", "A_StartMenu", "A_StartMenuCommon", "A_StartUp", "A_StartUpCommon",
|
||||||
|
"A_StringCaseSense", "A_Tab", "A_Temp", "A_ThisFunc", "A_ThisHotkey",
|
||||||
|
"A_ThisLabel", "A_ThisMenu", "A_ThisMenuItem", "A_ThisMenuItemPos", "A_TickCount",
|
||||||
|
"A_TimeIdle", "A_TimeIdlePhysical", "A_TimeSincePriorHotkey", "A_TimeSinceThisHotkey", "A_TitleMatchMode",
|
||||||
|
"A_TitleMatchModeSpeed", "A_UserName", "A_WDay", "A_WinDelay", "A_WinDir",
|
||||||
|
"A_WorkingDir", "A_YDay", "A_Year", "A_YWeek", "A_YYYY",
|
||||||
|
"CipboardAll", "Clipboard", "ComSpec", "ErrorLevel", "False",
|
||||||
|
"ProgramFiles", "True",
|
||||||
|
}
|
||||||
|
|
||||||
|
local keywords = {
|
||||||
|
"Break", "ByRef", "Case", "Catch", "Class",
|
||||||
|
"Continue", "Else", "Else", "Exit", "ExitApp",
|
||||||
|
"Finally", "For", "Global", "Gosub", "Goto",
|
||||||
|
"If", "Local", "Loop", "OnExit", "Pause",
|
||||||
|
"Return", "Sleep", "static", "suspend", "Switch",
|
||||||
|
"Throw", "Try", "Until", "While",
|
||||||
|
}
|
||||||
|
|
||||||
|
local functions = {
|
||||||
|
"_NewEnum", "Abs", "ACos", "Array", "Asc",
|
||||||
|
"ASin", "ATan", "Ceil", "Chr", "ComObjActive",
|
||||||
|
"ComObjArray", "ComObjConnect", "ComObjCreate", "ComObject", "ComObjError",
|
||||||
|
"ComObjFlags", "ComObjGet", "ComObjQuery", "ComObjType", "ComObjValue",
|
||||||
|
"Cos", "DllCall", "Exception", "Exp", "FileExist",
|
||||||
|
"FileOpen", "Floor", "Format", "Func", "GetKeyName",
|
||||||
|
"GetKeySC", "GetKeyState", "GetKeyVK", "IL_Add", "IL_Create",
|
||||||
|
"IL_Destroy", "InStr", "IsByRef", "IsFunc", "IsLabel",
|
||||||
|
"IsObject", "Ln", "Log", "LTrim", "LV_Add",
|
||||||
|
"LV_Delete", "LV_DeleteCol", "LV_GetCount", "LV_GetNext", "LV_GetText",
|
||||||
|
"LV_Insert", "LV_InsertCol", "LV_Modify", "LV_ModifyCol", "LV_SetImageList",
|
||||||
|
"Mod", "NumGet", "NumPut", "ObjAddRef", "ObjClone",
|
||||||
|
"Object", "ObjGetAddress", "ObjGetCapacity", "ObjHasKey", "ObjInsert",
|
||||||
|
"ObjMaxIndex", "ObjMinIndex", "ObjNewEnum", "ObjRelease", "ObjRemove",
|
||||||
|
"ObjSetCapacity", "OnMessage", "RegExMatch", "RegExReplace", "RegisterCallback",
|
||||||
|
"Round", "RTrim", "SB_SetIcon", "SB_SetParts", "SB_SetText",
|
||||||
|
"Sin", "Sqrt", "StrGet", "StrLen", "StrPut",
|
||||||
|
"StrReplace", "StrSplit", "SubStr", "Tan", "Trim",
|
||||||
|
"TV_Add", "TV_Delete", "TV_Get", "TV_GetChild", "TV_GetCount",
|
||||||
|
"TV_GetNext", "TV_GetParent", "TV_GetPrev", "TV_GetSelection", "TV_GetText",
|
||||||
|
"TV_Modify", "TV_SetImageList", "VarSetCapacity", "WinActive", "WinExist",
|
||||||
|
}
|
||||||
|
|
||||||
|
local commands = {
|
||||||
|
"AutoTrim", "BlockInput", "Click", "ClipWait", "Control",
|
||||||
|
"ControlClick", "ControlFocus", "ControlGet", "ControlGetFocus", "ControlGetPos",
|
||||||
|
"ControlGetText", "ControlMove", "ControlSend", "ControlSendRaw", "ControlSetText",
|
||||||
|
"CoordMode", "Critical", "DetectHiddenText", "DetectHiddenWindows", "Drive",
|
||||||
|
"DriveGet", "DriveSpaceFree", "Edit", "EnvAdd", "EnvGet",
|
||||||
|
"EnvSet", "EnvSub", "EnvUpdate", "FileAppend", "FileCopy",
|
||||||
|
"FileCopyDir", "FileCreateDir", "FileCreateShortcut", "FileDelete", "FileEncoding",
|
||||||
|
"FileGetAttrib", "FileGetShortcut", "FileGetSize", "FileGetTime", "FileGetVersion",
|
||||||
|
"FileInstall", "FileMove", "FileMoveDir", "FileRead", "FileReadLine",
|
||||||
|
"FileRecycle", "FileRecycleEmpty", "FileRemoveDir", "FileSelectFile", "FileSelectFolder",
|
||||||
|
"FileSetAttrib", "FileSetTime", "FormatTime", "GroupActivate", "GroupAdd",
|
||||||
|
"GroupClose", "GroupDeactivate", "Gui", "GuiControl", "GuiControlGet",
|
||||||
|
"Hotkey", "ImageSearch", "IniDelete", "IniRead", "IniWrite",
|
||||||
|
"Input", "InputBox", "KeyHistory", "KeyWait", "ListHotkeys",
|
||||||
|
"ListLines", "ListVars", "Menu", "MouseClick", "MouseClickDrag",
|
||||||
|
"MouseGetPos", "MouseMove", "MsgBox", "OutputDebug", "PixelGetColor",
|
||||||
|
"PixelSearch", "PostMessage", "Process", "Random", "RegDelete",
|
||||||
|
"RegRead", "RegWrite", "Reload", "Run", "RunAs",
|
||||||
|
"RunWait", "Send", "SendEvent", "SendInput", "SendLevel",
|
||||||
|
"SendMessage", "SendMode", "SendPlay", "SendRaw", "SetBatchLines",
|
||||||
|
"SetCapsLockState", "SetControlDelay", "SetDefaultMouseSpeed", "SetEnv", "SetKeyDelay",
|
||||||
|
"SetMouseDelay", "SetNumLockState", "SetRegView", "SetScrollLockState", "SetStoreCapsLockMode",
|
||||||
|
"SetTimer", "SetTitleMatchMode", "SetWinDelay", "SetWorkingDir", "Shutdown",
|
||||||
|
"Sort", "SoundBeep", "SoundGet", "SoundGetWaveVolume", "SoundPlay",
|
||||||
|
"SoundSet", "SoundSetWaveVolume", "Splitpath", "StatusBarGetText", "StatusBarWait",
|
||||||
|
"StringCaseSense", "StringLower", "StringUpper", "SysGet", "Thread",
|
||||||
|
"ToolTip", "Transform", "TrayTip", "UrlDownloadToFile", "WinActivate",
|
||||||
|
"WinActivateBottom", "WinClose", "WinGet", "WinGetActiveStats", "WinGetActiveTitle",
|
||||||
|
"WinGetClass", "WinGetPos", "WinGetText", "WinGetTitle", "WinHide",
|
||||||
|
"WinKill", "WinMaximize", "WinMenuSelectItem", "WinMinimize", "WinMinimizeAll",
|
||||||
|
"WinMinimizeAllUndo", "WinMove", "WinRestore", "WinSet", "WinSetTitle",
|
||||||
|
"WinShow", "WinWait", "WinWaitActive", "WinWaitClose", "WinWaitNotActive",
|
||||||
|
}
|
||||||
|
|
||||||
|
local symbols = {}
|
||||||
|
for _, elementVar in ipairs(variables) do
|
||||||
|
symbols[string.lower(elementVar)] = "operator" --a_year
|
||||||
|
symbols[string.upper(elementVar)] = "operator" --A_YEAR
|
||||||
|
symbols[string.format("%s%s", string.sub(elementVar, 1, 1), string.lower(string.sub(elementVar, 2)))] = "operator" --A_year
|
||||||
|
symbols[elementVar] = "operator" --A_Year
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, elementKeyword in ipairs(keywords) do
|
||||||
|
symbols[string.lower(elementKeyword)] = "function" --byref
|
||||||
|
symbols[string.upper(elementKeyword)] = "function" --BYREF
|
||||||
|
symbols[string.format("%s%s", string.sub(elementKeyword, 1, 1), string.lower(string.sub(elementKeyword, 2)))] = "function" --Byref
|
||||||
|
symbols[elementKeyword] = "function" --ByRef
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, elementFunction in ipairs(functions) do
|
||||||
|
symbols[string.lower(elementFunction)] = "keyword2" --lv_modifycol()
|
||||||
|
symbols[string.upper(elementFunction)] = "keyword2" --LV_MODIFYCOL()
|
||||||
|
symbols[string.format("%s%s", string.sub(elementFunction, 1, 1), string.lower(string.sub(elementFunction, 2)))] = "keyword2" --Lv_modifycol()
|
||||||
|
symbols[elementFunction] = "keyword2" --LV_ModifyCol()
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, elementCommands in ipairs(commands) do
|
||||||
|
symbols[string.lower(elementCommands)] = "keyword" --fileencoding
|
||||||
|
symbols[string.upper(elementCommands)] = "keyword" --FILEENCODING
|
||||||
|
symbols[string.format("%s%s", string.sub(elementCommands, 1, 1), string.lower(string.sub(elementCommands, 2)))] = "keyword" --Fileencoding
|
||||||
|
symbols[elementCommands] = "keyword" --FileEncoding
|
||||||
|
end
|
||||||
|
|
||||||
|
syntax.add {
|
||||||
|
name = "AutoHotkey",
|
||||||
|
files = { "%.ahk$"},
|
||||||
|
comment = ";",
|
||||||
|
patterns = {
|
||||||
|
{ pattern = { ";", "\n" }, type = "comment" },
|
||||||
|
{ pattern = { "/%*", "*%/" }, type = "comment" },
|
||||||
|
{ pattern = { "[ruU]?%%", "[%% ]", '\\' }, type = "operator" },
|
||||||
|
{ pattern = { "[ruU]?%%", "[ ,]", '\\' }, type = "normal" },
|
||||||
|
{ pattern = { '[ruU]?"""', '"""'; '\\' }, type = "string" },
|
||||||
|
{ pattern = { '[ruU]?"', '"', '\\' }, type = "string" },
|
||||||
|
{ pattern = "0x[%da-fA-F]+", type = "number" },
|
||||||
|
{ pattern = "-?%d+[%d%.eE]*", type = "number" },
|
||||||
|
{ pattern = "-?%.?%d+", type = "number" },
|
||||||
|
{ pattern = "[%+%-=/%*%^<>!~|&]", type = "operator" },
|
||||||
|
{ pattern = ":=", type = "operator" },
|
||||||
|
{ pattern = ".=", type = "operator" },
|
||||||
|
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },
|
||||||
|
{ pattern = "[%a_][%w_]*", type = "symbol" },
|
||||||
|
},
|
||||||
|
symbols = symbols,
|
||||||
|
}
|
||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
|
||||||
|
syntax.add {
|
||||||
|
name = "Awk script",
|
||||||
|
files = "%.awk$",
|
||||||
|
headers = "^#!.*bin.*awk",
|
||||||
|
comment = "#",
|
||||||
|
patterns = {
|
||||||
|
-- $# is a awk special variable and the '#' shouldn't be interpreted
|
||||||
|
-- as a comment.
|
||||||
|
{ pattern = "%$[%a_@*#][%w_]*", type = "keyword2" },
|
||||||
|
-- Comments
|
||||||
|
{ pattern = "#.*", type = "comment" },
|
||||||
|
-- Strings
|
||||||
|
{ pattern = { '"', '"', '\\' }, type = "string" },
|
||||||
|
{ pattern = { "'", "'", '\\' }, type = "string" },
|
||||||
|
{ pattern = { '`', '`', '\\' }, type = "string" },
|
||||||
|
-- Ignore numbers that start with dots or slashes
|
||||||
|
{ pattern = "%f[%w_%.%/]%d[%d%.]*%f[^%w_%.]", type = "number" },
|
||||||
|
-- Operators
|
||||||
|
{ pattern = "[!<>|&%[%]:=*]", type = "operator" },
|
||||||
|
-- Match parameters
|
||||||
|
{ pattern = "%f[%S][%+%-][%w%-_:]+", type = "function" },
|
||||||
|
{ pattern = "%f[%S][%+%-][%w%-_]+%f[=]", type = "function" },
|
||||||
|
-- Prevent parameters with assignments from been matched as variables
|
||||||
|
{
|
||||||
|
pattern = "%s%-%a[%w_%-]*()%s+()%d[%d%.]+",
|
||||||
|
type = { "function", "normal", "number" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern = "%s%-%a[%w_%-]*()%s+()%a[%a%-_:=]+",
|
||||||
|
type = { "function", "normal", "symbol" }
|
||||||
|
},
|
||||||
|
-- Match variable assignments
|
||||||
|
{ pattern = "[_%a][%w_]+%f[%+=]", type = "keyword2" },
|
||||||
|
-- Match variable expansions
|
||||||
|
{ pattern = "%${.-}", type = "keyword2" },
|
||||||
|
{ pattern = "%$[%d%$%a_@*][%w_]*", type = "keyword2" },
|
||||||
|
-- Functions
|
||||||
|
{ pattern = "[%a_%-][%w_%-]*()%s*%f[(]", type = { "function", "normal" } },
|
||||||
|
-- Everything else
|
||||||
|
{ pattern = "[%a_][%w_]*", type = "symbol" },
|
||||||
|
},
|
||||||
|
symbols = {
|
||||||
|
["break"] = "keyword",
|
||||||
|
["continue"] = "keyword",
|
||||||
|
["do"] = "keyword",
|
||||||
|
["delete"] = "keyword",
|
||||||
|
["else"] = "keyword",
|
||||||
|
["exit"] = "keyword",
|
||||||
|
["for"] = "keyword",
|
||||||
|
["function"] = "keyword",
|
||||||
|
["getline"] = "keyword",
|
||||||
|
["if"] = "keyword",
|
||||||
|
["next"] = "keyword",
|
||||||
|
["nextfile"] = "keyword",
|
||||||
|
["print"] = "keyword",
|
||||||
|
["printf"] = "keyword",
|
||||||
|
["return"] = "keyword",
|
||||||
|
["while"] = "keyword",
|
||||||
|
["gsub"] = "keyword",
|
||||||
|
["index"] = "keyword",
|
||||||
|
["length"] = "keyword",
|
||||||
|
["match"] = "keyword",
|
||||||
|
["split"] = "keyword",
|
||||||
|
["sprintf"] = "keyword",
|
||||||
|
["sub"] = "keyword",
|
||||||
|
["substr"] = "keyword",
|
||||||
|
["tolower"] = "keyword",
|
||||||
|
["toupper"] = "keyword",
|
||||||
|
["atan2"] = "keyword",
|
||||||
|
["cos"] = "keyword",
|
||||||
|
["exp"] = "keyword",
|
||||||
|
["int"] = "keyword",
|
||||||
|
["log"] = "keyword",
|
||||||
|
["rand"] = "keyword",
|
||||||
|
["sin"] = "keyword",
|
||||||
|
["sqrt"] = "keyword",
|
||||||
|
["srand"] = "keyword",
|
||||||
|
["BEGIN"] = "keyword",
|
||||||
|
["END"] = "keyword",
|
||||||
|
["ARGC"] = "keyword",
|
||||||
|
["ARGV"] = "keyword",
|
||||||
|
["FILENAME"] = "keyword",
|
||||||
|
["FNR"] = "keyword",
|
||||||
|
["FS"] = "keyword",
|
||||||
|
["NF"] = "keyword",
|
||||||
|
["NR"] = "keyword",
|
||||||
|
["OFMT"] = "keyword",
|
||||||
|
["OFS"] = "keyword",
|
||||||
|
["ORS"] = "keyword",
|
||||||
|
["RLENGTH"] = "keyword",
|
||||||
|
["RS"] = "keyword",
|
||||||
|
["RSTART"] = "keyword",
|
||||||
|
["SUBSEP"] = "keyword",
|
||||||
|
["ARGIND"] = "keyword",
|
||||||
|
["BINMODE"] = "keyword",
|
||||||
|
["CONVFMT"] = "keyword",
|
||||||
|
["ENVIRON"] = "keyword",
|
||||||
|
["ERRNO"] = "keyword",
|
||||||
|
["FIELDWIDTHS"] = "keyword",
|
||||||
|
["IGNORECASE"] = "keyword",
|
||||||
|
["LINT"] = "keyword",
|
||||||
|
["PROCINFO"] = "keyword",
|
||||||
|
["RT"] = "keyword",
|
||||||
|
["RLENGTH"] = "keyword",
|
||||||
|
["TEXTDOMAIN"] = "keyword"
|
||||||
|
}
|
||||||
|
}
|
||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
|
||||||
|
-- batch syntax for lite <liqube>
|
||||||
|
|
||||||
|
-- windows batch files use caseless matching for symbols
|
||||||
|
local symtable = {
|
||||||
|
["keyword"] = {
|
||||||
|
"if", "else", "not", "for", "do", "in",
|
||||||
|
"equ", "neq", "lss", "leq", "gtr", "geq", -- == != < <= > >=
|
||||||
|
"nul", "con", "prn", "prn", "lpt1", "com1", "com2", "com3", "com4",
|
||||||
|
"exist", "defined",
|
||||||
|
"errorlevel", "cmdextversion",
|
||||||
|
"goto", "call", "verify",
|
||||||
|
},
|
||||||
|
["function"] = {
|
||||||
|
"set", "setlocal", "endlocal", "enabledelayedexpansion",
|
||||||
|
"echo", "type",
|
||||||
|
"cd", "chdir",
|
||||||
|
"md", "mkdir",
|
||||||
|
"pause", "choice", "exit",
|
||||||
|
"del", "rd", "rmdir",
|
||||||
|
"copy", "xcopy",
|
||||||
|
"move", "ren",
|
||||||
|
"find", "findstr",
|
||||||
|
"sort", "shift", "attrib",
|
||||||
|
"cmd", "command",
|
||||||
|
"forfiles",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
-- prepare a mixed symbol list
|
||||||
|
local function prepare_symbols(symtable)
|
||||||
|
local symbols = { }
|
||||||
|
for symtype, symlist in pairs(symtable) do
|
||||||
|
for _, symname in ipairs(symlist) do
|
||||||
|
symbols[symname:lower()] = symtype
|
||||||
|
symbols[symname:upper()] = symtype
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return symbols
|
||||||
|
end
|
||||||
|
|
||||||
|
syntax.add {
|
||||||
|
name = "Batch",
|
||||||
|
files = { "%.bat$", "%.cmd$" },
|
||||||
|
comment = "rem",
|
||||||
|
patterns = {
|
||||||
|
{ pattern = "@echo off\n", type = "keyword" },
|
||||||
|
{ pattern = "@echo on\n", type = "keyword" },
|
||||||
|
{ pattern = "rem.-\n", type = "comment" }, -- rem comment line, rem, rem.
|
||||||
|
{ pattern = "REM.-\n", type = "comment" },
|
||||||
|
{ pattern = "%s*:[%w%-]+", type = "symbol" }, -- :labels
|
||||||
|
{ pattern = "%:%:.-\n", type = "comment" }, -- :: comment line
|
||||||
|
{ pattern = "%%%w+%%", type = "symbol" }, -- %variable%
|
||||||
|
{ pattern = "%%%%?~?[%w:]+", type = "symbol" }, -- %1, %~dpn1, %~1:2, %%i, %%~i
|
||||||
|
{ pattern = "[!=()%>&%^/\\@]", type = "operator" }, -- operators
|
||||||
|
{ pattern = "-?%.?%d+f?", type = "number" }, -- integer numbers
|
||||||
|
{ pattern = { '"', '"', '\\' }, type = "string" }, -- "strings"
|
||||||
|
{ pattern = "[%a_][%w_]*", type = "normal" },
|
||||||
|
{ pattern = ":eof", type = "keyword" }, -- not quite as intended, but ok for now
|
||||||
|
},
|
||||||
|
symbols = prepare_symbols(symtable),
|
||||||
|
}
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
-- Author: Rohan Vashisht https://github.com/RohanVashisht1234
|
||||||
|
|
||||||
|
-- mod-version:3
|
||||||
|
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
|
||||||
|
syntax.add {
|
||||||
|
name = "Bend",
|
||||||
|
files = { "%.bend$" },
|
||||||
|
comment = "#",
|
||||||
|
patterns = {
|
||||||
|
{ pattern = "#.*", type = "comment" },
|
||||||
|
{ pattern = { '"', '"' }, type = "string" },
|
||||||
|
{ pattern = { "'", "'" }, type = "string" },
|
||||||
|
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },
|
||||||
|
{ pattern = "[%a_][%w_]*", type = "symbol" },
|
||||||
|
{ pattern = "[%+%-=/%*%^%%<>!|&]", type = "operator" },
|
||||||
|
{ pattern = "def()%s+()[%a_][%w_]*", type = { "keyword", "normal", "literal" } }, -- tested ok
|
||||||
|
{ pattern = "data()%s+()[%a_][%w_]*", type = { "keyword", "normal", "literal" } }, -- tested ok
|
||||||
|
{ pattern = "let()%s+()[%a_][%w_]*", type = { "keyword", "normal", "literal" } }, -- tested ok
|
||||||
|
{ pattern = "Some()%s+()[%a_][%w_]*", type = { "keyword", "normal", "literal" } }, -- tested ok
|
||||||
|
{ pattern = "bend()%s+()[%a_][%w_]*", type = { "keyword", "normal", "literal" } }, -- tested ok
|
||||||
|
{ pattern = "object()%s+()[%a_][%w_]*", type = { "keyword", "normal", "literal" } }, -- tested ok
|
||||||
|
{ pattern = "fold()%s+()[%a_][%w_]*", type = { "keyword", "normal", "literal" } }, -- tested ok
|
||||||
|
{ pattern = "open()%s+()[%a_][%w_]*", type = { "keyword", "normal", "literal" } }, -- tested ok
|
||||||
|
{ pattern = "do()%s+()[%a_][%w_]*", type = { "keyword", "normal", "literal" } }, -- tested ok
|
||||||
|
{ pattern = "identity()%s+()[%a_][%w_]*", type = { "keyword", "normal", "literal" } }, -- tested ok
|
||||||
|
{ pattern = "lambda()%s+()[%a_][%w_]*", type = { "keyword", "normal", "literal" } }, -- tested ok
|
||||||
|
{ pattern = "0x[%da-fA-F]+", type = "number" },
|
||||||
|
{ pattern = "-?%d+[%d%.eE]*", type = "number" },
|
||||||
|
{ pattern = "-?%.?%d+", type = "number" },
|
||||||
|
},
|
||||||
|
symbols = {
|
||||||
|
["def"] = "keyword",
|
||||||
|
["switch"] = "keyword",
|
||||||
|
["case"] = "keyword",
|
||||||
|
["return"] = "keyword",
|
||||||
|
["if"] = "keyword",
|
||||||
|
["else"] = "keyword",
|
||||||
|
["when"] = "keyword",
|
||||||
|
["match"] = "keyword",
|
||||||
|
["λ"] = "keyword",
|
||||||
|
["Some"] = "keyword",
|
||||||
|
["data"] = "keyword",
|
||||||
|
["let"] = "keyword",
|
||||||
|
["use"] = "keyword",
|
||||||
|
["object"] = "keyword",
|
||||||
|
["fold"] = "keyword",
|
||||||
|
["open"] = "keyword",
|
||||||
|
["do"] = "keyword",
|
||||||
|
["bind"] = "keyword",
|
||||||
|
["Name"] = "keyword",
|
||||||
|
["identity"] = "keyword",
|
||||||
|
["Bool"] = "keyword",
|
||||||
|
["ask"] = "keyword",
|
||||||
|
["with"] = "keyword",
|
||||||
|
["bend"] = "keyword2",
|
||||||
|
["None"] = "keyword2",
|
||||||
|
["Nil"] = "keyword2",
|
||||||
|
["Result"] = "keyword2",
|
||||||
|
["type"] = "keyword2",
|
||||||
|
["true"] = "literal",
|
||||||
|
["false"] = "literal",
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
|
||||||
|
syntax.add {
|
||||||
|
name = "BibTeX",
|
||||||
|
files = { "%.bib$" },
|
||||||
|
comment = "%%",
|
||||||
|
patterns = {
|
||||||
|
{ pattern = {"%%", "\n"}, type = "comment" },
|
||||||
|
{ pattern = "@%a+", type = "keyword" },
|
||||||
|
{ pattern = "%a+%s=", type = "keyword2" },
|
||||||
|
},
|
||||||
|
symbols = {
|
||||||
|
["author"] = "keyword",
|
||||||
|
["doi"] = "keyword",
|
||||||
|
["issue"] = "keyword",
|
||||||
|
["journal"] = "keyword",
|
||||||
|
["month"] = "keyword",
|
||||||
|
["numpages"] = "keyword",
|
||||||
|
["pages"] = "keyword",
|
||||||
|
["publisher"] = "keyword",
|
||||||
|
}
|
||||||
|
}
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
|
||||||
|
syntax.add {
|
||||||
|
name = "Blade",
|
||||||
|
files = { "%.b$" },
|
||||||
|
comment = "#",
|
||||||
|
patterns = {
|
||||||
|
{ pattern = "#.*", type = "comment" },
|
||||||
|
{ pattern = { "/%*", "%*/" }, type = "comment" },
|
||||||
|
{ pattern = { '"', '"', '\\' }, type = "string" },
|
||||||
|
{ pattern = { "'", "'", '\\' }, type = "string" },
|
||||||
|
{ pattern = "%d+%.%d+", type = "number" },
|
||||||
|
{ pattern = "0x%x+", type = "number" },
|
||||||
|
{ pattern = "0c[0-8]+", type = "number" },
|
||||||
|
{ pattern = "0c[01]+", type = "number" },
|
||||||
|
{ pattern = "%d+", type = "number" },
|
||||||
|
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
|
||||||
|
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },
|
||||||
|
{ pattern = "%a[%w_]+", type = "symbol" },
|
||||||
|
},
|
||||||
|
-- https://bladelang.com/tutorial/reserved.html#reserved-words
|
||||||
|
symbols = {
|
||||||
|
["and"] = "keyword",
|
||||||
|
["continue"] = "keyword",
|
||||||
|
["else"] = "keyword",
|
||||||
|
["in"] = "keyword",
|
||||||
|
["self"] = "keyword2",
|
||||||
|
["when"] = "keyword",
|
||||||
|
["as"] = "keyword",
|
||||||
|
["def"] = "keyword",
|
||||||
|
["iter"] = "keyword",
|
||||||
|
["static"] = "keyword",
|
||||||
|
["while"] = "keyword",
|
||||||
|
["assert"] = "keyword",
|
||||||
|
["default"] = "keyword",
|
||||||
|
["finally"] = "keyword",
|
||||||
|
["break"] = "keyword",
|
||||||
|
["die"] = "keyword",
|
||||||
|
["for"] = "keyword",
|
||||||
|
["or"] = "keyword",
|
||||||
|
["try"] = "keyword",
|
||||||
|
["catch"] = "keyword",
|
||||||
|
["do"] = "keyword",
|
||||||
|
["if"] = "keyword",
|
||||||
|
["parent"] = "keyword",
|
||||||
|
["using"] = "keyword",
|
||||||
|
["class"] = "keyword",
|
||||||
|
["echo"] = "function",
|
||||||
|
["import"] = "keyword",
|
||||||
|
["return"] = "keyword",
|
||||||
|
["var"] = "keyword",
|
||||||
|
["true"] = "literal",
|
||||||
|
["false"] = "literal",
|
||||||
|
["nil"] = "literal",
|
||||||
|
},
|
||||||
|
}
|
||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
|
||||||
|
syntax.add {
|
||||||
|
name = "Blueprint",
|
||||||
|
files = { "%.blp$", },
|
||||||
|
comment = "//",
|
||||||
|
block_comment = {"/*", "*/"},
|
||||||
|
patterns = {
|
||||||
|
|
||||||
|
-- Comments
|
||||||
|
{ pattern = "//.*", type = "comment" },
|
||||||
|
{ pattern = { "/%*", "%*/" }, type = "comment" },
|
||||||
|
|
||||||
|
-- Strings
|
||||||
|
{ pattern = { "'", "'", "\\"}, type = "string" },
|
||||||
|
{ pattern = { '"', '"', "\\" }, type = "string" },
|
||||||
|
|
||||||
|
-- Numbers
|
||||||
|
{ pattern = "%.?%d+", type = "number" },
|
||||||
|
|
||||||
|
-- Child type
|
||||||
|
{ pattern = "%[.*%]", type = "literal" },
|
||||||
|
|
||||||
|
-- Operators
|
||||||
|
{ pattern = "%$", type = "operator" },
|
||||||
|
{ pattern = "=>%s*%$().*()%(%)", type = { "operator", "function", "normal" } },
|
||||||
|
|
||||||
|
-- Properties
|
||||||
|
{ pattern = "[%w-_]+()%s*:", type = { "keyword", "normal" } },
|
||||||
|
|
||||||
|
-- Classes
|
||||||
|
{ pattern = "[%w_-%.]+%s*(){", type = { "keyword2", "normal"} },
|
||||||
|
{ pattern = "[%w_-%.]+%s*()[%w_-]+%s*{", type = { "keyword2", "normal"} },
|
||||||
|
|
||||||
|
-- Symbols
|
||||||
|
{ pattern = "[%w-_]+", type = "symbol" },
|
||||||
|
|
||||||
|
},
|
||||||
|
symbols = {
|
||||||
|
["true"] = "literal",
|
||||||
|
["false"] = "literal",
|
||||||
|
["null"] = "literal",
|
||||||
|
|
||||||
|
-- Import statements
|
||||||
|
["using"] = "keyword",
|
||||||
|
|
||||||
|
-- Keywords
|
||||||
|
["after"] = "keyword",
|
||||||
|
["bidirectional"] = "keyword",
|
||||||
|
["bind-property"] = "keyword",
|
||||||
|
["bind"] = "keyword",
|
||||||
|
["default"] = "keyword",
|
||||||
|
["destructive"] = "keyword",
|
||||||
|
["disabled"] = "keyword",
|
||||||
|
["inverted"] = "keyword",
|
||||||
|
["no-sync-create"] = "keyword",
|
||||||
|
["suggested"] = "keyword",
|
||||||
|
["swapped"] = "keyword",
|
||||||
|
["sync-create"] = "keyword",
|
||||||
|
["template"] = "keyword",
|
||||||
|
|
||||||
|
-- Menus
|
||||||
|
["menu"] = "keyword",
|
||||||
|
["submenu"] = "keyword",
|
||||||
|
["section"] = "keyword",
|
||||||
|
|
||||||
|
-- Nested blocks
|
||||||
|
["responses"] = "keyword2",
|
||||||
|
["items"] = "keyword2",
|
||||||
|
["mime-types"] = "keyword2",
|
||||||
|
["patterns"] = "keyword2",
|
||||||
|
["suffixes"] = "keyword2",
|
||||||
|
["marks"] = "keyword2",
|
||||||
|
["widgets"] = "keyword2",
|
||||||
|
["strings"] = "keyword2",
|
||||||
|
["styles"] = "keyword2",
|
||||||
|
["accessibility"] = "keyword2",
|
||||||
|
["setters"] = "keyword2",
|
||||||
|
["layout"] = "keyword2",
|
||||||
|
["item"] = "keyword2",
|
||||||
|
["condition"] = "keyword2",
|
||||||
|
["mark"] = "keyword2",
|
||||||
|
|
||||||
|
-- Translated strings
|
||||||
|
["_"] = "operator",
|
||||||
|
["C_"] = "operator",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
-- Author: Rohan Vashisht: https://github.com/RohanVashisht1234/
|
||||||
|
|
||||||
|
-- mod-version:3
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
|
||||||
|
syntax.add {
|
||||||
|
name = "Brainfuck", -- tested ok
|
||||||
|
files = {
|
||||||
|
"%.bf$", -- tested ok
|
||||||
|
},
|
||||||
|
patterns = {
|
||||||
|
{ pattern = '%[', type = 'operator' }, -- tested ok
|
||||||
|
{ pattern = '%]', type = 'operator' }, -- tested ok
|
||||||
|
{ pattern = '%-', type = 'keyword' }, -- tested ok
|
||||||
|
{ pattern = '<', type = 'keyword2' }, -- tested ok
|
||||||
|
{ pattern = '>', type = 'keyword2' }, -- tested ok
|
||||||
|
{ pattern = '+', type = 'string' }, -- tested ok
|
||||||
|
{ pattern = ',', type = 'literal' }, -- tested ok
|
||||||
|
{ pattern = '%.', type = 'string' }, -- tested ok
|
||||||
|
{ pattern = '[^%-%.<>%+,%[%]]+', type = 'comment' }, -- tested ok
|
||||||
|
},
|
||||||
|
symbols = {},
|
||||||
|
}
|
||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
-- Author: Rohan Vashisht: https://github.com/rohanvashisht1234/
|
||||||
|
|
||||||
|
|
||||||
|
-- mod-version:3
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
|
||||||
|
syntax.add {
|
||||||
|
name = "Buzz", -- tested ok
|
||||||
|
files = { "%.buzz$" }, -- tested ok
|
||||||
|
comment = "|", -- tested ok
|
||||||
|
patterns = {
|
||||||
|
{ pattern = { '"', '"', '\\' }, type = "string" }, -- tested ok
|
||||||
|
{ pattern = "|.*", type = "comment" }, -- tested ok
|
||||||
|
{ pattern = "[!%-/*?:=><]", type = "operator" }, -- tested ok
|
||||||
|
{ pattern = "[%a_][%w_]*%f[(]", type = "function" }, -- tested ok
|
||||||
|
{ pattern = "const()%s+()[%a_][%w_]*", type = { "keyword", "normal", "literal" } }, -- tested ok
|
||||||
|
{ pattern = "object()%s+()[%a_][%w_]*", type = { "keyword", "normal", "literal" } }, -- tested ok
|
||||||
|
{ pattern = "var()%s+()[%a_][%w_]*", type = { "keyword", "normal", "literal" } }, -- tested ok
|
||||||
|
{ pattern = "-?%d+[%d%.eE_]*", type = "number" }, -- tested ok
|
||||||
|
{ pattern = "-?%.?%d+", type = "number" }, -- tested ok
|
||||||
|
{ pattern = "[%a_][%w_]*", type = "normal" }, -- tested ok
|
||||||
|
},
|
||||||
|
symbols = {
|
||||||
|
["bool"] = "keyword", -- tested ok
|
||||||
|
["ud"] = "keyword", -- tested ok
|
||||||
|
["float"] = "keyword", -- tested ok
|
||||||
|
["zdef"] = "keyword", -- tested ok
|
||||||
|
["resolve"] = "keyword", -- tested ok
|
||||||
|
["yield"] = "keyword", -- tested ok
|
||||||
|
["resume"] = "keyword", -- tested ok
|
||||||
|
["export"] = "keyword", -- tested ok
|
||||||
|
["void"] = "keyword", -- tested ok
|
||||||
|
["protocol"] = "keyword", -- tested ok
|
||||||
|
["do"] = "keyword", -- tested ok
|
||||||
|
["enum"] = "keyword", -- tested ok
|
||||||
|
["test"] = "keyword", -- tested ok
|
||||||
|
["extern"] = "keyword", -- tested ok
|
||||||
|
["object"] = "keyword", -- tested ok
|
||||||
|
["foreach"] = "keyword", -- tested ok
|
||||||
|
["is"] = "keyword", -- tested ok
|
||||||
|
["return"] = "keyword", -- tested ok
|
||||||
|
["continue"] = "keyword", -- tested ok
|
||||||
|
["for"] = "keyword", -- tested ok
|
||||||
|
["lambda"] = "keyword", -- tested ok
|
||||||
|
["try"] = "keyword", -- tested ok
|
||||||
|
["fun"] = "keyword", -- tested ok
|
||||||
|
["type"] = "keyword", -- tested ok
|
||||||
|
["while"] = "keyword", -- tested ok
|
||||||
|
["and"] = "keyword", -- tested ok
|
||||||
|
["global"] = "keyword", -- tested ok
|
||||||
|
["not"] = "keyword2", -- tested ok
|
||||||
|
["any"] = "keyword", -- tested ok
|
||||||
|
["as"] = "keyword", -- tested ok
|
||||||
|
["if"] = "keyword", -- tested ok
|
||||||
|
["or"] = "keyword", -- tested ok
|
||||||
|
["else"] = "keyword", -- tested ok
|
||||||
|
["match"] = "keyword", -- tested ok
|
||||||
|
["pat"] = "keyword", -- tested ok
|
||||||
|
["import"] = "keyword", -- tested ok
|
||||||
|
["str"] = "keyword", -- tested ok
|
||||||
|
["var"] = "keyword", -- tested ok
|
||||||
|
["catch"] = "keyword", -- tested ok
|
||||||
|
["typeof"] = "keyword", -- tested ok
|
||||||
|
["int"] = "keyword", -- tested ok
|
||||||
|
["const"] = "keyword", -- tested ok
|
||||||
|
["namespace"] = "keyword", -- tested ok
|
||||||
|
["this"] = "keyword2", -- tested ok
|
||||||
|
["null"] = "literal", -- tested ok
|
||||||
|
["true"] = "literal", -- tested ok
|
||||||
|
["false"] = "literal", -- tested ok
|
||||||
|
["in"] = "literal", -- tested ok
|
||||||
|
["static"] = "keyword2", -- tested ok
|
||||||
|
["std"] = "keyword2", -- tested ok
|
||||||
|
["io"] = "keyword2", -- tested ok
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+80
@@ -0,0 +1,80 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
|
||||||
|
syntax.add {
|
||||||
|
name = "cel7",
|
||||||
|
files = "%.c7$",
|
||||||
|
comment = ";",
|
||||||
|
patterns = {
|
||||||
|
{ pattern = ";.*", type = "comment" },
|
||||||
|
{ pattern = { '"', '"', '\\' }, type = "string" },
|
||||||
|
{ pattern = "0x4000", type = "literal" },
|
||||||
|
{ pattern = "0x4040", type = "literal" },
|
||||||
|
{ pattern = "0x52a0", type = "literal" },
|
||||||
|
{ pattern = "0x[%da-fA-F]+", type = "number" },
|
||||||
|
{ pattern = "-?%d+[%d%.]*", type = "number" },
|
||||||
|
{ pattern = "-?%.?%d+", type = "number" },
|
||||||
|
{ pattern = "'", type = "symbol" },
|
||||||
|
{ pattern = "=", type = "symbol" },
|
||||||
|
{ pattern = "<=?", type = "symbol" },
|
||||||
|
{ pattern = "[%+-%*/]", type = "symbol" },
|
||||||
|
{ pattern = "//", type = "keyword2" },
|
||||||
|
{ pattern = "%%", type = "keyword2" },
|
||||||
|
{ pattern = "%f[^(][^()'%s\"]+", type = "function" },
|
||||||
|
{ pattern = "[^()'%s\"]+", type = "symbol" },
|
||||||
|
},
|
||||||
|
symbols = {
|
||||||
|
["let"] = "keyword",
|
||||||
|
["="] = "operator",
|
||||||
|
["if"] = "keyword",
|
||||||
|
["fn"] = "keyword",
|
||||||
|
["mac"] = "keyword",
|
||||||
|
["while"] = "keyword",
|
||||||
|
["quote"] = "keyword",
|
||||||
|
["'"] = "keyword",
|
||||||
|
["and"] = "keyword",
|
||||||
|
["or"] = "keyword",
|
||||||
|
["do"] = "keyword",
|
||||||
|
["cons"] = "keyword",
|
||||||
|
["car"] = "keyword",
|
||||||
|
["cdr"] = "keyword",
|
||||||
|
["setcar"] = "keyword",
|
||||||
|
["setcdr"] = "keyword",
|
||||||
|
["list"] = "keyword",
|
||||||
|
["not"] = "keyword",
|
||||||
|
["is"] = "keyword",
|
||||||
|
["atom"] = "keyword",
|
||||||
|
["print"] = "keyword",
|
||||||
|
["<"] = "operator",
|
||||||
|
["<="] = "operator",
|
||||||
|
["="] = "operator",
|
||||||
|
["+"] = "operator",
|
||||||
|
["-"] = "operator",
|
||||||
|
["*"] = "operator",
|
||||||
|
["/"] = "operator",
|
||||||
|
["nil"] = "literal",
|
||||||
|
["t"] = "literal",
|
||||||
|
|
||||||
|
-- reserved variables (config)
|
||||||
|
["title"] = "keyword2",
|
||||||
|
["width"] = "keyword2",
|
||||||
|
["height"] = "keyword2",
|
||||||
|
["debug"] = "keyword2",
|
||||||
|
|
||||||
|
-- callbacks
|
||||||
|
["init"] = "keyword2",
|
||||||
|
["step"] = "keyword2",
|
||||||
|
["keydown"] = "keyword2",
|
||||||
|
["keyup"] = "keyword2",
|
||||||
|
|
||||||
|
-- built-in functions
|
||||||
|
["quit"] = "keyword2",
|
||||||
|
["rand"] = "keyword2",
|
||||||
|
["poke"] = "keyword2",
|
||||||
|
["peek"] = "keyword2",
|
||||||
|
["color"] = "keyword2",
|
||||||
|
["put"] = "keyword2",
|
||||||
|
["get"] = "keyword2",
|
||||||
|
["fill"] = "keyword2",
|
||||||
|
}
|
||||||
|
}
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
|
||||||
|
syntax.add {
|
||||||
|
files = { PATHSEP .. "Caddyfile$" },
|
||||||
|
comment = "#",
|
||||||
|
patterns = {
|
||||||
|
{ pattern = "#.*", type = "comment" },
|
||||||
|
{ pattern = { '"', '"', '\\' }, type = "string" },
|
||||||
|
-- Matcher definition
|
||||||
|
{ pattern = "@[%w_]+", type = "operator" },
|
||||||
|
-- Snippet
|
||||||
|
{ pattern = "%(%g+%)", type = "operator" },
|
||||||
|
-- Properties
|
||||||
|
{ pattern = "^[%a_][%w_]*()%s+%f[%g]",
|
||||||
|
type = { "function", "normal" }
|
||||||
|
},
|
||||||
|
{ pattern = "^[%a_][%w_]*()%s+$",
|
||||||
|
type = { "function", "normal" }
|
||||||
|
},
|
||||||
|
{ pattern = "^%s*()[%a_][%w_]*()%s+$",
|
||||||
|
type = { "normal", "function", "normal" }
|
||||||
|
},
|
||||||
|
{ pattern = "^%s*()[%a_][%w_]*()%s+%f[%g]",
|
||||||
|
type = { "normal", "function", "normal" }
|
||||||
|
},
|
||||||
|
-- Environment variables
|
||||||
|
{ pattern = "{()%$[%w_]+():()[%w_]+()}",
|
||||||
|
type = { "operator", "keyword2", "operator", "keyword2", "operator" }
|
||||||
|
},
|
||||||
|
{ pattern = "{()%$[%w_]+()}",
|
||||||
|
type = { "operator", "keyword2", "operator" }
|
||||||
|
},
|
||||||
|
-- Place holder
|
||||||
|
{ pattern = "{%g-}", type = "keyword2" },
|
||||||
|
-- Operators
|
||||||
|
{ pattern = "[+%-,:]", type = "operator" },
|
||||||
|
-- IP Address
|
||||||
|
{ pattern = "%d+%.%d+%.%d+%.%d+", type = "literal" },
|
||||||
|
-- Path /path/subpath
|
||||||
|
{ pattern = "/[%w%./]+", type = "literal" },
|
||||||
|
-- Wildcard domain *.levels
|
||||||
|
{ pattern = "%*()[%w.]+",
|
||||||
|
type = { "operator", "literal" }
|
||||||
|
},
|
||||||
|
-- Match Operator
|
||||||
|
{ pattern = "%*+", type = "operator" },
|
||||||
|
-- Domain leve1.level2
|
||||||
|
{ pattern = "https?://[%w%./%*]+", type = "literal" },
|
||||||
|
-- Domain leve1.level2
|
||||||
|
{ pattern = "%w+%.[%w%.]+", type = "literal" },
|
||||||
|
-- Number
|
||||||
|
{ pattern = "%d+[mhskbi]*", type = "number" },
|
||||||
|
-- Everything else for symbols to work
|
||||||
|
{ pattern = "[%a_][%w_]*", type = "symbol" },
|
||||||
|
},
|
||||||
|
symbols = {
|
||||||
|
["true"] = "literal",
|
||||||
|
["false"] = "literal",
|
||||||
|
["localhost"] = "literal",
|
||||||
|
|
||||||
|
-- built-in directives
|
||||||
|
["abort"] = "keyword",
|
||||||
|
["acme_server"] = "keyword",
|
||||||
|
["basicauth"] = "keyword",
|
||||||
|
["bind"] = "keyword",
|
||||||
|
["encode"] = "keyword",
|
||||||
|
["error"] = "keyword",
|
||||||
|
["file_server"] = "keyword",
|
||||||
|
["forward_auth"] = "keyword",
|
||||||
|
["handle"] = "keyword",
|
||||||
|
["handle_errors"] = "keyword",
|
||||||
|
["handle_path"] = "keyword",
|
||||||
|
["header"] = "keyword",
|
||||||
|
["import"] = "keyword",
|
||||||
|
["log"] = "keyword",
|
||||||
|
["method"] = "keyword",
|
||||||
|
["map"] = "keyword",
|
||||||
|
["metrics"] = "keyword",
|
||||||
|
["php_fastcgi"] = "keyword",
|
||||||
|
["push"] = "keyword",
|
||||||
|
["redir"] = "keyword",
|
||||||
|
["request_body"] = "keyword",
|
||||||
|
["request_header"] = "keyword",
|
||||||
|
["respond"] = "keyword",
|
||||||
|
["reverse_proxy"] = "keyword",
|
||||||
|
["rewrite"] = "keyword",
|
||||||
|
["root"] = "keyword",
|
||||||
|
["route"] = "keyword",
|
||||||
|
["templates"] = "keyword",
|
||||||
|
["tls"] = "keyword",
|
||||||
|
["tracing"] = "keyword",
|
||||||
|
["try_files"] = "keyword",
|
||||||
|
["uri"] = "keyword",
|
||||||
|
["vars"] = "keyword",
|
||||||
|
|
||||||
|
-- Module directives
|
||||||
|
["cgi"] = "keyword",
|
||||||
|
["ssh"] = "keyword",
|
||||||
|
["exec"] = "keyword",
|
||||||
|
["supervisor"] = "keyword",
|
||||||
|
["layer4"] = "keyword",
|
||||||
|
},
|
||||||
|
}
|
||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
-- Author: Rohan Vashisht: https://github.com/rohanvashisht1234/
|
||||||
|
|
||||||
|
-- mod-version:3
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
|
||||||
|
syntax.add {
|
||||||
|
name = "Carbon", -- tested ok
|
||||||
|
files = {
|
||||||
|
"%.carbon$" -- tested ok
|
||||||
|
},
|
||||||
|
comment = "//", -- tested ok
|
||||||
|
patterns = {
|
||||||
|
{ pattern = { '"', '"', '\\' }, type = "string" }, -- tested ok
|
||||||
|
{ pattern = { '"""', '"""', '\\' }, type = "string" }, -- tested ok
|
||||||
|
{ pattern = { "'''", "'''", '\\' }, type = "string" }, -- tested ok
|
||||||
|
{ pattern = "//.*", type = "comment" }, -- tested ok
|
||||||
|
{ pattern = "[!%-/*?:=><+]", type = "operator" }, -- tested ok
|
||||||
|
{ pattern = "[%a_][%w_]*%f[(]", type = "function" }, -- tested ok
|
||||||
|
{ pattern = "packages()%s+()[%a_][%w_]*", type = { "keyword", "normal", "literal" } }, -- tested ok
|
||||||
|
{ pattern = "let()%s+()[%a_][%w_]*", type = { "keyword", "normal", "literal" } }, -- tested ok
|
||||||
|
{ pattern = "import()%s+()[%a_][%w_]*", type = { "keyword", "normal", "literal" } }, -- tested ok
|
||||||
|
{ pattern = "impl()%s+()[%a_][%w_]*", type = { "keyword", "normal", "literal" } }, -- tested ok
|
||||||
|
{ pattern = "class()%s+()[%a_][%w_]*", type = { "keyword", "normal", "literal" } }, -- tested ok
|
||||||
|
{ pattern = "var()%s+()[%a_][%w_]*", type = { "keyword", "normal", "literal" } }, -- tested ok
|
||||||
|
{ pattern = "package()%s+()[%a_][%w_]*", type = { "keyword", "normal", "literal" } }, -- tested ok
|
||||||
|
{ pattern = "-?%d+[%d%.eE_]*", type = "number" }, -- tested ok
|
||||||
|
{ pattern = "-?%.?%d+", type = "number" }, -- tested ok
|
||||||
|
{ pattern = "[%a_][%w_]*", type = "normal" }, -- tested ok
|
||||||
|
},
|
||||||
|
symbols = {
|
||||||
|
["package"] = "keyword", -- tested ok
|
||||||
|
["import"] = "keyword", -- tested ok
|
||||||
|
["fn"] = "keyword", -- tested ok
|
||||||
|
["var"] = "keyword", -- tested ok
|
||||||
|
["for"] = "keyword", -- tested ok
|
||||||
|
["return"] = "keyword", -- tested ok
|
||||||
|
["class"] = "keyword", -- tested ok
|
||||||
|
["api"] = "keyword", -- tested ok
|
||||||
|
["i8"] = "keyword", -- tested ok
|
||||||
|
["i16"] = "keyword", -- tested ok
|
||||||
|
["i32"] = "keyword", -- tested ok
|
||||||
|
["i64"] = "keyword", -- tested ok
|
||||||
|
["i128"] = "keyword", -- tested ok
|
||||||
|
["i256"] = "keyword", -- tested ok
|
||||||
|
["u8"] = "keyword", -- tested ok
|
||||||
|
["u16"] = "keyword", -- tested ok
|
||||||
|
["u32"] = "keyword", -- tested ok
|
||||||
|
["u64"] = "keyword", -- tested ok
|
||||||
|
["u128"] = "keyword", -- tested ok
|
||||||
|
["u256"] = "keyword", -- tested ok
|
||||||
|
["f8"] = "keyword", -- tested ok
|
||||||
|
["f16"] = "keyword", -- tested ok
|
||||||
|
["f32"] = "keyword", -- tested ok
|
||||||
|
["f64"] = "keyword", -- tested ok
|
||||||
|
["f128"] = "keyword", -- tested ok
|
||||||
|
["if"] = "keyword", -- tested ok
|
||||||
|
["else"] = "keyword", -- tested ok
|
||||||
|
["auto"] = "keyword", -- tested ok
|
||||||
|
["let"] = "keyword", -- tested ok
|
||||||
|
["File"] = "keyword", -- tested ok
|
||||||
|
["while"] = "keyword", -- tested ok
|
||||||
|
["match"] = "keyword", -- tested ok
|
||||||
|
["case"] = "keyword", -- tested ok
|
||||||
|
["default"] = "keyword", -- tested ok
|
||||||
|
["returned"] = "keyword", -- tested ok
|
||||||
|
["base"] = "keyword", -- tested ok
|
||||||
|
["bool"] = "keyword", -- tested ok
|
||||||
|
["virtual"] = "keyword", -- tested ok
|
||||||
|
["abstract"] = "keyword", -- tested ok
|
||||||
|
["String"] = "keyword", -- tested ok
|
||||||
|
|
||||||
|
["impl"] = "keyword2", -- tested ok
|
||||||
|
["extend"] = "keyword", -- tested ok
|
||||||
|
["partial"] = "keyword2", -- tested ok
|
||||||
|
["Self"] = "keyword", -- tested ok
|
||||||
|
["Int"] = "keyword", -- tested ok
|
||||||
|
["UInt"] = "keyword", -- tested ok
|
||||||
|
["Base"] = "keyword", -- tested ok
|
||||||
|
["template"] = "keyword2", -- tested ok
|
||||||
|
["true"] = "keyword2", -- tested ok
|
||||||
|
["false"] = "keyword2", -- tested ok
|
||||||
|
}
|
||||||
|
}
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
-- Author: Rohan Vashisht: https://github.com/RohanVashisht1234/
|
||||||
|
|
||||||
|
-- mod-version:3
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
syntax.add {
|
||||||
|
name = "Clojure",
|
||||||
|
comment = ";;",
|
||||||
|
files = {
|
||||||
|
"%.clj$",
|
||||||
|
"%.cljs$",
|
||||||
|
"%.clc$",
|
||||||
|
"%.edn$",
|
||||||
|
},
|
||||||
|
patterns = {
|
||||||
|
{ pattern = ';;.*', type = 'comment' }, -- Single-line Comment
|
||||||
|
{ pattern = ';.*', type = 'comment' }, -- Single-line Comment
|
||||||
|
{ pattern = { '#"', '"', '\\' }, type = 'string' }, -- Multiline String
|
||||||
|
{ pattern = { '"', '"', '\\' }, type = 'string' }, -- Multiline String
|
||||||
|
{ pattern = { '"""', '"""', '\\' }, type = 'string' }, -- Multiline String
|
||||||
|
{ pattern = ':[%a_][%w_/%-]*', type = 'keyword2' }, -- word after ':' a.k.a (Var metadata)
|
||||||
|
{ pattern = '[%a_][%w_]*()%.()[%a_][%w_/%-]*', type = { 'keyword', 'operator', 'keyword2' } }, -- Things like something.something
|
||||||
|
{ pattern = "%(()def()%s+()[%a_][%w_%-]*", type = { "normal", "keyword", "literal", 'literal' } }, -- function definition
|
||||||
|
{ pattern = "%(()def[%a_][%w_]*()%s+()[%a_][%w_%-]*", type = { "normal", "keyword", "literal", 'literal' } }, -- function definition but with something along with def like: defn, defmacro etc.
|
||||||
|
{ pattern = '%(()require()%s+()[%a_][%w_]*', type = { 'normal', 'keyword', 'literal', 'literal' } }, -- highlight the word after require keyword
|
||||||
|
{ pattern = '%(()[%a_][%w_/]*', type = { 'normal', 'literal' } }, -- patterns that are like this: (my_function/subdir_1)
|
||||||
|
{ pattern = '-?0x%x+', type = 'number' }, -- Hexadecimal
|
||||||
|
{ pattern = '-?%d+[%d%.eE]*f?', type = 'number' }, -- Floating-point numbers
|
||||||
|
{ pattern = '-?%.?%d+f?', type = 'number' }, -- Floating-point numbers
|
||||||
|
{ pattern = '[!%#%$%%&*+./%<=>%?@\\%^|%-~:]', type = 'operator' }, -- Character classes
|
||||||
|
{ pattern = "[%a_'][%w_']*", type = 'normal' }, -- Normal
|
||||||
|
},
|
||||||
|
symbols = {
|
||||||
|
['def'] = 'keyword', -- tested ok
|
||||||
|
['defn'] = 'keyword', -- tested ok
|
||||||
|
['str'] = 'keyword', -- tested ok
|
||||||
|
['fn'] = 'keyword', -- tested ok
|
||||||
|
['println'] = 'keyword', -- tested ok
|
||||||
|
['if'] = 'keyword', -- tested ok
|
||||||
|
['cond'] = 'keyword', -- tested ok
|
||||||
|
['vector'] = 'keyword', -- tested ok
|
||||||
|
['apply'] = 'keyword', -- tested ok
|
||||||
|
['String'] = 'keyword', -- tested ok
|
||||||
|
['ns'] = 'keyword', -- tested ok
|
||||||
|
['try'] = 'keyword', -- tested ok
|
||||||
|
['let'] = 'keyword', -- tested ok
|
||||||
|
['get'] = 'keyword', -- tested ok
|
||||||
|
['catch'] = 'keyword', -- tested ok
|
||||||
|
['Retention'] = 'keyword', -- tested ok
|
||||||
|
['Deprecated'] = 'keyword', -- tested ok
|
||||||
|
['require'] = 'keyword2', -- tested ok
|
||||||
|
['true'] = 'keyword2', -- tested ok
|
||||||
|
['false'] = 'keyword2', -- tested ok
|
||||||
|
['nil'] = 'literal', -- tested ok
|
||||||
|
['int'] = 'literal', -- tested ok
|
||||||
|
},
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
|
||||||
|
syntax.add {
|
||||||
|
name = "CMake",
|
||||||
|
files = { "%.cmake$", PATHSEP .. "CMakeLists%.txt$" },
|
||||||
|
comment = "#",
|
||||||
|
block_comment = { "#[[", "]]" },
|
||||||
|
patterns = {
|
||||||
|
{ pattern = { '#%[=*%[', '%]=*%]' }, type = "comment" },
|
||||||
|
{ pattern = "#.*", type = "comment" },
|
||||||
|
{ pattern = { '%[=*%[', '%]=*%]' }, type = "string" },
|
||||||
|
{ pattern = { '"', '"', '\\' }, type = "string" },
|
||||||
|
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },
|
||||||
|
{ pattern = "[%a_][%w_]*", type = "normal" },
|
||||||
|
{ pattern = "%${[%a_][%w_]*%}", type = "operator" },
|
||||||
|
},
|
||||||
|
symbols = {},
|
||||||
|
}
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
|
||||||
|
syntax.add {
|
||||||
|
name = "C#",
|
||||||
|
files = "%.cs$",
|
||||||
|
comment = "//",
|
||||||
|
patterns = {
|
||||||
|
{ pattern = "//.-\n", type = "comment" },
|
||||||
|
{ pattern = { "/%*", "%*/" }, type = "comment" },
|
||||||
|
{ pattern = { '"', '"', '\\' }, type = "string" },
|
||||||
|
{ pattern = { "[%$%@]?\"", '"', '\\' }, type = "string" }, -- string interpolation and verbatim
|
||||||
|
{ pattern = "'\\x%x?%x?%x?%x'", type = "string" }, -- character hexadecimal escape sequence
|
||||||
|
{ pattern = "'\\u%x%x%x%x'", type = "string" }, -- character unicode escape sequence
|
||||||
|
{ pattern = "'\\?.'", type = "string" }, -- character literal
|
||||||
|
{ pattern = "-?0x%x+", type = "number" },
|
||||||
|
{ pattern = "-?%d+[%d%.eE]*f?", type = "number" },
|
||||||
|
{ pattern = "-?%.?%d+f?", type = "number" },
|
||||||
|
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
|
||||||
|
{ pattern = "%?%?", type = "operator" }, -- ?? null-coalescing
|
||||||
|
{ pattern = "%?%.", type = "operator" }, -- ?. null-conditional
|
||||||
|
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },
|
||||||
|
{ pattern = "[%a_][%w_]*", type = "symbol" },
|
||||||
|
},
|
||||||
|
symbols = {
|
||||||
|
-- keywords and contextual keywords
|
||||||
|
["abstract"] = "keyword",
|
||||||
|
["as"] = "keyword",
|
||||||
|
["add"] = "keyword",
|
||||||
|
["await"] = "keyword",
|
||||||
|
["base"] = "keyword",
|
||||||
|
["break"] = "keyword",
|
||||||
|
["case"] = "keyword",
|
||||||
|
["catch"] = "keyword",
|
||||||
|
["checked"] = "keyword",
|
||||||
|
["class"] = "keyword",
|
||||||
|
["record"] = "keyword",
|
||||||
|
["const"] = "keyword",
|
||||||
|
["continue"] = "keyword",
|
||||||
|
["default"] = "keyword",
|
||||||
|
["delegate"] = "keyword",
|
||||||
|
["do"] = "keyword",
|
||||||
|
["else"] = "keyword",
|
||||||
|
["enum"] = "keyword",
|
||||||
|
["event"] = "keyword",
|
||||||
|
["explicit"] = "keyword",
|
||||||
|
["extern"] = "keyword",
|
||||||
|
["finally"] = "keyword",
|
||||||
|
["fixed"] = "keyword",
|
||||||
|
["for"] = "keyword",
|
||||||
|
["foreach"] = "keyword",
|
||||||
|
["get"] = "keyword",
|
||||||
|
["goto"] = "keyword",
|
||||||
|
["if"] = "keyword",
|
||||||
|
["implicit"] = "keyword",
|
||||||
|
["in"] = "keyword",
|
||||||
|
["interface"] = "keyword",
|
||||||
|
["internal"] = "keyword",
|
||||||
|
["is"] = "keyword",
|
||||||
|
["lock"] = "keyword",
|
||||||
|
["namespace"] = "keyword",
|
||||||
|
["new"] = "keyword",
|
||||||
|
["operator"] = "keyword",
|
||||||
|
["out"] = "keyword",
|
||||||
|
["override"] = "keyword",
|
||||||
|
["remove"] = "keyword",
|
||||||
|
["params"] = "keyword",
|
||||||
|
["partial"] = "keyword",
|
||||||
|
["private"] = "keyword",
|
||||||
|
["protected"] = "keyword",
|
||||||
|
["dynamic"] = "keyword",
|
||||||
|
["public"] = "keyword",
|
||||||
|
["readonly"] = "keyword",
|
||||||
|
["ref"] = "keyword",
|
||||||
|
["return"] = "keyword",
|
||||||
|
["sealed"] = "keyword",
|
||||||
|
["set"] = "keyword",
|
||||||
|
["sizeof"] = "keyword",
|
||||||
|
["stackalloc"] = "keyword",
|
||||||
|
["static"] = "keyword",
|
||||||
|
["struct"] = "keyword",
|
||||||
|
["switch"] = "keyword",
|
||||||
|
["this"] = "keyword",
|
||||||
|
["throw"] = "keyword",
|
||||||
|
["try"] = "keyword",
|
||||||
|
["typeof"] = "keyword",
|
||||||
|
["unchecked"] = "keyword",
|
||||||
|
["unsafe"] = "keyword",
|
||||||
|
["using"] = "keyword",
|
||||||
|
["var"] = "keyword",
|
||||||
|
["value"] = "keyword",
|
||||||
|
["global"] = "keyword",
|
||||||
|
["virtual"] = "keyword",
|
||||||
|
["void"] = "keyword",
|
||||||
|
["volatile"] = "keyword",
|
||||||
|
["where"] = "keyword",
|
||||||
|
["when"] = "keyword",
|
||||||
|
["while"] = "keyword",
|
||||||
|
["yield"] = "keyword",
|
||||||
|
-- types
|
||||||
|
["bool"] = "keyword2",
|
||||||
|
["byte"] = "keyword2",
|
||||||
|
["char"] = "keyword2",
|
||||||
|
["decimal"] = "keyword2",
|
||||||
|
["double"] = "keyword2",
|
||||||
|
["float"] = "keyword2",
|
||||||
|
["int"] = "keyword2",
|
||||||
|
["long"] = "keyword2",
|
||||||
|
["object"] = "keyword2",
|
||||||
|
["sbyte"] = "keyword2",
|
||||||
|
["short"] = "keyword2",
|
||||||
|
["string"] = "keyword2",
|
||||||
|
["uint"] = "keyword2",
|
||||||
|
["ulong"] = "keyword2",
|
||||||
|
["ushort"] = "keyword2",
|
||||||
|
-- literals
|
||||||
|
["true"] = "literal",
|
||||||
|
["false"] = "literal",
|
||||||
|
["null"] = "literal",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
|
||||||
|
syntax.add {
|
||||||
|
name = "CUE",
|
||||||
|
files = "%.cue$",
|
||||||
|
comment = "//",
|
||||||
|
patterns = {
|
||||||
|
{ pattern = "//.*", type = "comment" },
|
||||||
|
{ pattern = { '"', '"', '\\' }, type = "string" },
|
||||||
|
{ pattern = { "`", "`", '\\' }, type = "string" },
|
||||||
|
{ pattern = { "'", "'", '\\' }, type = "string" },
|
||||||
|
{ pattern = "0[oO_][0-7]+i?", type = "number" },
|
||||||
|
{ pattern = "-?0x[%x_]+i?", type = "number" },
|
||||||
|
{ pattern = "-?%d+_%di?", type = "number" },
|
||||||
|
{ pattern = "-?%d+[%d%.eE]*f?i?", type = "number" },
|
||||||
|
{ pattern = "-?%.?%d+f?i?", type = "number" },
|
||||||
|
{ pattern = "[%a_][%w_]*%.", type = "literal" },
|
||||||
|
{ pattern = "[%a_][%w_]*", type = "symbol" },
|
||||||
|
{ pattern = "#[%a][%w_]*", type = "keyword2" },
|
||||||
|
-- operators
|
||||||
|
{ pattern = "[%+%-=/%*%^%%<>!~|&%?:%.]", type = "operator" },
|
||||||
|
},
|
||||||
|
symbols = {
|
||||||
|
["package"] = "keyword",
|
||||||
|
["import"] = "keyword",
|
||||||
|
["let"] = "keyword",
|
||||||
|
["for"] = "keyword",
|
||||||
|
["true"] = "literal",
|
||||||
|
["false"] = "literal",
|
||||||
|
["string"] = "keyword2",
|
||||||
|
["bool"] = "keyword2",
|
||||||
|
["number"] = "keyword2",
|
||||||
|
["uint32"] = "keyword2",
|
||||||
|
["int32"] = "keyword2",
|
||||||
|
["uint16"] = "keyword2",
|
||||||
|
["int16"] = "keyword2",
|
||||||
|
["uint8"] = "keyword2",
|
||||||
|
["float"] = "keyword2",
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+135
@@ -0,0 +1,135 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
|
||||||
|
syntax.add {
|
||||||
|
name = "D",
|
||||||
|
files = { "%.d$", "%.di$" },
|
||||||
|
comment = "//",
|
||||||
|
patterns = {
|
||||||
|
{ pattern = "//.-\n", type = "comment" },
|
||||||
|
{ pattern = { "/%*", "%*/" }, type = "comment" },
|
||||||
|
{ pattern = { "/%+", "%+/" }, type = "comment" },
|
||||||
|
{ pattern = { '`', '`', '\\' }, type = "string" },
|
||||||
|
{ pattern = { '"', '"', '\\' }, type = "string" },
|
||||||
|
{ pattern = { "'", "'", '\\' }, type = "string" },
|
||||||
|
{ pattern = "-?0x[%x_]+", type = "number" },
|
||||||
|
{ pattern = "-?[%d_]+[%d%.eE]*f?", type = "number" },
|
||||||
|
{ pattern = "-?%.?%d+f?", type = "number" },
|
||||||
|
{ pattern = "[%+%-=/%*%^%%<>!~|&%$]+", type = "operator" },
|
||||||
|
{ pattern = "[%a_][%w_]*!?()[%(.]", type = {"function", "normal"} }, -- highlight templates
|
||||||
|
{ pattern = "@safe", type = "keyword" },
|
||||||
|
{ pattern = "@trusted", type = "keyword" },
|
||||||
|
{ pattern = "@nogc", type = "keyword" },
|
||||||
|
},
|
||||||
|
symbols = {
|
||||||
|
["abstract"] = "keyword",
|
||||||
|
["alias"] = "keyword",
|
||||||
|
["align"] = "keyword",
|
||||||
|
["asm"] = "keyword",
|
||||||
|
["assert"] = "keyword",
|
||||||
|
["auto"] = "keyword",
|
||||||
|
["body"] = "keyword",
|
||||||
|
["bool"] = "keyword2",
|
||||||
|
["break"] = "keyword",
|
||||||
|
["byte"] = "keyword2",
|
||||||
|
["case"] = "keyword",
|
||||||
|
["cast"] = "keyword",
|
||||||
|
["catch"] = "keyword",
|
||||||
|
["cdouble"] = "keyword2",
|
||||||
|
["cent"] = "keyword2",
|
||||||
|
["cfloat"] = "keyword2",
|
||||||
|
["char"] = "keyword2",
|
||||||
|
["class"] = "keyword",
|
||||||
|
["const"] = "keyword",
|
||||||
|
["continue"] = "keyword",
|
||||||
|
["creal"] = "keyword2",
|
||||||
|
["dchar"] = "keyword2",
|
||||||
|
["debug"] = "keyword",
|
||||||
|
["default"] = "keyword",
|
||||||
|
["delegate"] = "keyword",
|
||||||
|
["deprecated"] = "keyword",
|
||||||
|
["do"] = "keyword",
|
||||||
|
["double"] = "keyword2",
|
||||||
|
["else"] = "keyword",
|
||||||
|
["enum"] = "keyword",
|
||||||
|
["export"] = "keyword",
|
||||||
|
["extern"] = "keyword",
|
||||||
|
["false"] = "literal",
|
||||||
|
["final"] = "keyword",
|
||||||
|
["finally"] = "keyword",
|
||||||
|
["float"] = "keyword2",
|
||||||
|
["for"] = "keyword",
|
||||||
|
["foreach"] = "keyword",
|
||||||
|
["foreach_reverse"] = "keyword",
|
||||||
|
["function"] = "keyword",
|
||||||
|
["goto"] = "keyword",
|
||||||
|
["idouble"] = "keyword2",
|
||||||
|
["if"] = "keyword",
|
||||||
|
["ifloat"] = "keyword2",
|
||||||
|
["immutable"] = "keyword",
|
||||||
|
["import"] = "keyword",
|
||||||
|
["in"] = "keyword",
|
||||||
|
["inout"] = "keyword",
|
||||||
|
["int"] = "keyword2",
|
||||||
|
["interface"] = "keyword",
|
||||||
|
["invariant"] = "keyword",
|
||||||
|
["ireal"] = "keyword2",
|
||||||
|
["is"] = "keyword",
|
||||||
|
["lazy"] = "keyword",
|
||||||
|
["long"] = "keyword2",
|
||||||
|
["macro"] = "keyword",
|
||||||
|
["mixin"] = "keyword",
|
||||||
|
["module"] = "keyword",
|
||||||
|
["new"] = "keyword",
|
||||||
|
["nothrow"] = "keyword",
|
||||||
|
["null"] = "literal",
|
||||||
|
["out"] = "keyword",
|
||||||
|
["override"] = "keyword",
|
||||||
|
["package"] = "keyword",
|
||||||
|
["pragma"] = "keyword",
|
||||||
|
["private"] = "keyword",
|
||||||
|
["protected"] = "keyword",
|
||||||
|
["public"] = "keyword",
|
||||||
|
["pure"] = "keyword",
|
||||||
|
["real"] = "keyword2",
|
||||||
|
["ref"] = "keyword",
|
||||||
|
["return"] = "keyword",
|
||||||
|
["scope"] = "keyword",
|
||||||
|
["shared"] = "keyword",
|
||||||
|
["short"] = "keyword2",
|
||||||
|
["static"] = "keyword",
|
||||||
|
["struct"] = "keyword",
|
||||||
|
["super"] = "keyword",
|
||||||
|
["switch"] = "keyword",
|
||||||
|
["synchronized"] = "keyword",
|
||||||
|
["template"] = "keyword",
|
||||||
|
["this"] = "keyword",
|
||||||
|
["throw"] = "keyword",
|
||||||
|
["true"] = "literal",
|
||||||
|
["try"] = "keyword",
|
||||||
|
["typeid"] = "keyword",
|
||||||
|
["typeof"] = "keyword",
|
||||||
|
["ubyte"] = "keyword2",
|
||||||
|
["ucent"] = "keyword2",
|
||||||
|
["uint"] = "keyword2",
|
||||||
|
["ulong"] = "keyword2",
|
||||||
|
["union"] = "keyword",
|
||||||
|
["unittest"] = "keyword",
|
||||||
|
["ushort"] = "keyword2",
|
||||||
|
["version"] = "keyword",
|
||||||
|
["void"] = "keyword",
|
||||||
|
["wchar"] = "keyword2",
|
||||||
|
["while"] = "keyword",
|
||||||
|
["with"] = "keyword",
|
||||||
|
["__FILE__"] = "keyword",
|
||||||
|
["__FILE_FULL_PATH__"] = "keyword",
|
||||||
|
["__MODULE__"] = "keyword",
|
||||||
|
["__LINE__"] = "keyword",
|
||||||
|
["__FUNCTION__"] = "keyword",
|
||||||
|
["__PRETTY_FUNCTION__"] = "keyword",
|
||||||
|
["__gshared"] = "keyword",
|
||||||
|
["__traits"] = "keyword",
|
||||||
|
["__vector"] = "keyword",
|
||||||
|
["__parameters"] = "keyword",
|
||||||
|
},
|
||||||
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
|
||||||
|
syntax.add {
|
||||||
|
name = "Dart",
|
||||||
|
files = { "%.dart$" },
|
||||||
|
comment = "//",
|
||||||
|
patterns = {
|
||||||
|
{ pattern = "//.-\n", type = "comment" },
|
||||||
|
{ pattern = "///.-\n", type = "comment" },
|
||||||
|
{ pattern = { "/%*", "%*/" }, type = "comment" },
|
||||||
|
{ pattern = { '"', '"', '\\' }, type = "string" },
|
||||||
|
{ pattern = { "'", "'", '\\' }, type = "string" },
|
||||||
|
{ pattern = "-?0x%x+", type = "number" },
|
||||||
|
{ pattern = "-?%d+[%d%.eE]*f?", type = "number" },
|
||||||
|
{ pattern = "-?%.?%d+f?", type = "number" },
|
||||||
|
{ pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" },
|
||||||
|
{ pattern = "%?%?", type = "operator" },
|
||||||
|
{ pattern = "%?%.", type = "operator" },
|
||||||
|
{ pattern = { "[%$%@]?\"", '"', '\\' }, type = "string" },
|
||||||
|
{ pattern = "'\\x%x?%x?%x?%x'", type = "string" },
|
||||||
|
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },
|
||||||
|
{ pattern = "[%a_][%w_]*", type = "symbol" },
|
||||||
|
},
|
||||||
|
symbols = {
|
||||||
|
["await"] = "keyword",
|
||||||
|
["bool"] = "keyword2",
|
||||||
|
["break"] = "keyword",
|
||||||
|
["case"] = "keyword",
|
||||||
|
["class"] = "keyword",
|
||||||
|
["const"] = "keyword",
|
||||||
|
["continue"] = "keyword",
|
||||||
|
["default"] = "keyword",
|
||||||
|
["do"] = "keyword",
|
||||||
|
["double"] = "keyword2",
|
||||||
|
["dynamic"] = "keyword2",
|
||||||
|
["else"] = "keyword",
|
||||||
|
["enum"] = "keyword",
|
||||||
|
["false"] = "literal",
|
||||||
|
["final"] = "keyword",
|
||||||
|
["finally"] = "keyword",
|
||||||
|
["for"] = "keyword",
|
||||||
|
["Function"] = "keyword2",
|
||||||
|
["if"] = "keyword",
|
||||||
|
["in"] = "keyword",
|
||||||
|
["int"] = "keyword2",
|
||||||
|
["List"] = "keyword2",
|
||||||
|
["Map"] = "keyword2",
|
||||||
|
["new"] = "keyword",
|
||||||
|
["null"] = "literal",
|
||||||
|
["part of"] = "keyword",
|
||||||
|
["print"] = "keyword",
|
||||||
|
["return"] = "keyword",
|
||||||
|
["static"] = "keyword",
|
||||||
|
["String"] = "keyword2",
|
||||||
|
["switch"] = "keyword",
|
||||||
|
["then"] = "keyword",
|
||||||
|
["this"] = "keyword2",
|
||||||
|
["true"] = "literal",
|
||||||
|
["void"] = "keyword",
|
||||||
|
["while"] = "keyword",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
local style = require "core.style"
|
||||||
|
local common = require "core.common"
|
||||||
|
|
||||||
|
-- we need these symbol types to have uniform colors
|
||||||
|
style.syntax["diff_add"] = { common.color "#72b886" }
|
||||||
|
style.syntax["diff_del"] = { common.color "#F36161" }
|
||||||
|
|
||||||
|
syntax.add {
|
||||||
|
name = "Diff",
|
||||||
|
files = { "%.diff$", "%.patch$", "%.rej$" },
|
||||||
|
headers = "^diff %-",
|
||||||
|
patterns = {
|
||||||
|
-- Method the patch was generated with and source/target files
|
||||||
|
{ regex = "^diff .+", type = "function" },
|
||||||
|
-- Seen for changing the file permissions
|
||||||
|
{ regex = "^new .+", type = "comment" },
|
||||||
|
-- Usually holds starting and ending commit
|
||||||
|
{ regex = "^index .+", type = "comment" },
|
||||||
|
-- Position to patch
|
||||||
|
{
|
||||||
|
pattern = "@@.-@@ ().+", --with heading
|
||||||
|
type = { "number", "string" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex = "^@@ [\\d,\\-\\+ ]+ @@\n", --wihtout heading
|
||||||
|
type = "number"
|
||||||
|
},
|
||||||
|
-- Other position to patch formats
|
||||||
|
{
|
||||||
|
regex = "^-{3} [\\d]+,[\\d]+ \\-{4}\n",
|
||||||
|
type = "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
regex = "^\\*{3} [\\d]+,[\\d]+ \\*{4}\n",
|
||||||
|
type = "number"
|
||||||
|
},
|
||||||
|
-- Source and target file
|
||||||
|
{ regex = "^-{3} .+", type = "keyword" },
|
||||||
|
{ regex = "^\\+{3} .+", type = "keyword" },
|
||||||
|
-- Rarely used source file indicator
|
||||||
|
{ regex = "^\\*{3} .+", type = "keyword" },
|
||||||
|
-- git patches seem to add 3 dashes to separate message from changed files
|
||||||
|
{ regex = "^-{3}\n", type = "normal" },
|
||||||
|
-- Addition and deletion of lines
|
||||||
|
{ regex = "^-.*", type = "diff_del" },
|
||||||
|
{ regex = "^\\+.*", type = "diff_add" },
|
||||||
|
{ regex = "^<.*", type = "diff_del" },
|
||||||
|
{ regex = "^>.*", type = "diff_add" },
|
||||||
|
-- Change between two lines
|
||||||
|
{ regex = "^!.*", type = "number" },
|
||||||
|
-- Stuff usually found on a authored patch heading
|
||||||
|
{
|
||||||
|
pattern = "From ()[a-fA-F0-9]+ ().+",
|
||||||
|
type = { "keyword", "number", "string" }
|
||||||
|
},
|
||||||
|
{ regex = "^[a-zA-Z\\-]+: ", type = "keyword" },
|
||||||
|
-- Diff stats
|
||||||
|
{ regex = "^ [\\d]+ files? changed", type = "function" },
|
||||||
|
{ regex = "[\\d]+ insertions?\\(\\+\\)", type = "diff_add" },
|
||||||
|
{ regex = "[\\d]+ deletions?\\(\\-\\)", type = "diff_del" },
|
||||||
|
-- Match e-mail
|
||||||
|
{
|
||||||
|
pattern = ".*()<[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+%.[a-zA-Z0-9-.]+>",
|
||||||
|
type = {"string", "keyword2"}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
symbols = {}
|
||||||
|
}
|
||||||
+645
@@ -0,0 +1,645 @@
|
|||||||
|
-- mod-version:3
|
||||||
|
local syntax = require "core.syntax"
|
||||||
|
|
||||||
|
syntax.add {
|
||||||
|
name = "FreeFEM++",
|
||||||
|
files = {
|
||||||
|
"%.edp$", "%.ffp$"
|
||||||
|
},
|
||||||
|
comment = "//",
|
||||||
|
block_comment = { "/*", "*/" },
|
||||||
|
patterns = {
|
||||||
|
{ pattern = "//.*", type = "comment" },
|
||||||
|
{ pattern = { "/%*", "%*/" }, type = "comment" },
|
||||||
|
{ pattern = { '"', '"', '\\' }, type = "string" },
|
||||||
|
{ pattern = "0x%x+[%x']*", type = "number" },
|
||||||
|
{ pattern = "%d+[%d%.'eE]*f?", type = "number" },
|
||||||
|
{ pattern = "%.?%d+[%d']*f?", type = "number" },
|
||||||
|
{ pattern = "[%+%-=/%*%^%%<>!~|:&]", type = "operator" },
|
||||||
|
{ pattern = "##", type = "operator" },
|
||||||
|
{ pattern = "struct%s()[%a_][%w_]*", type = { "keyword", "keyword2" } },
|
||||||
|
{ pattern = "class%s()[%a_][%w_]*", type = { "keyword", "keyword2" } },
|
||||||
|
{ pattern = "union%s()[%a_][%w_]*", type = { "keyword", "keyword2" } },
|
||||||
|
{ pattern = "namespace%s()[%a_][%w_]*", type = { "keyword", "keyword2" } },
|
||||||
|
-- static declarations
|
||||||
|
{
|
||||||
|
pattern = "static()%s+()inline",
|
||||||
|
type = { "keyword", "normal", "keyword" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern = "static()%s+()const",
|
||||||
|
type = { "keyword", "normal", "keyword" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern = "static()%s+()[%a_][%w_]*",
|
||||||
|
type = { "keyword", "normal", "literal" }
|
||||||
|
},
|
||||||
|
-- match method type declarations
|
||||||
|
{
|
||||||
|
pattern = "[%a_][%w_]*()%s*()%**()%s*()[%a_][%w_]*()%s*()::",
|
||||||
|
type = {
|
||||||
|
"literal", "normal", "operator", "normal",
|
||||||
|
"literal", "normal", "operator"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
-- match function type declarations
|
||||||
|
{
|
||||||
|
pattern = "[%a_][%w_]*()%*+()%s+()[%a_][%w_]*%f[%(]",
|
||||||
|
type = { "literal", "operator", "normal", "function" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern = "[%a_][%w_]*()%s+()%*+()[%a_][%w_]*%f[%(]",
|
||||||
|
type = { "literal", "normal", "operator", "function" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*%f[%(]",
|
||||||
|
type = { "literal", "normal", "function" }
|
||||||
|
},
|
||||||
|
-- match variable type declarations
|
||||||
|
{
|
||||||
|
pattern = "[%a_][%w_]*()%*+()%s+()[%a_][%w_]*",
|
||||||
|
type = { "literal", "operator", "normal", "normal" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern = "[%a_][%w_]*()%s+()%*+()[%a_][%w_]*",
|
||||||
|
type = { "literal", "normal", "operator", "normal" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*()%s*()[;,%[%)]",
|
||||||
|
type = { "literal", "normal", "normal", "normal", "normal" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*()%s*()=",
|
||||||
|
type = { "literal", "normal", "normal", "normal", "operator" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern = "[%a_][%w_]*()&()%s+()[%a_][%w_]*",
|
||||||
|
type = { "literal", "operator", "normal", "normal" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern = "[%a_][%w_]*()%s+()&()[%a_][%w_]*",
|
||||||
|
type = { "literal", "normal", "operator", "normal" }
|
||||||
|
},
|
||||||
|
-- Match scope operator element access
|
||||||
|
{
|
||||||
|
pattern = "[%a_][%w_]*()%s*()::",
|
||||||
|
type = { "literal", "normal", "operator" }
|
||||||
|
},
|
||||||
|
-- Uppercase constants of at least 2 chars in len
|
||||||
|
{
|
||||||
|
pattern = "_?%u[%u_][%u%d_]*%f[%s%+%*%-%.%)%]}%?%^%%=/<>~|&;:,!]",
|
||||||
|
type = "number"
|
||||||
|
},
|
||||||
|
-- Magic constants
|
||||||
|
{ pattern = "__[%u%l]+__", type = "number" },
|
||||||
|
-- all other functions
|
||||||
|
{ pattern = "[%a_][%w_]*%f[(]", type = "function" },
|
||||||
|
-- Macros
|
||||||
|
{
|
||||||
|
pattern = "^%s*#%s*define%s+()[%a_][%a%d_]*",
|
||||||
|
type = { "keyword", "symbol" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern = "#%s*include%s+()<.->",
|
||||||
|
type = { "keyword", "string" }
|
||||||
|
},
|
||||||
|
{ pattern = "%f[#]#%s*[%a_][%w_]*", type = "keyword" },
|
||||||
|
-- Everything else to make the tokenizer work properly
|
||||||
|
{ pattern = "[%a_][%w_]*", type = "symbol" },
|
||||||
|
},
|
||||||
|
symbols = {
|
||||||
|
["alignof"] = "keyword",
|
||||||
|
["alignas"] = "keyword",
|
||||||
|
["and"] = "keyword",
|
||||||
|
["and_eq"] = "keyword",
|
||||||
|
["not"] = "keyword",
|
||||||
|
["not_eq"] = "keyword",
|
||||||
|
["or"] = "keyword",
|
||||||
|
["or_eq"] = "keyword",
|
||||||
|
["xor"] = "keyword",
|
||||||
|
["xor_eq"] = "keyword",
|
||||||
|
["private"] = "keyword",
|
||||||
|
["protected"] = "keyword",
|
||||||
|
["public"] = "keyword",
|
||||||
|
["register"] = "keyword",
|
||||||
|
["nullptr"] = "keyword",
|
||||||
|
["operator"] = "keyword",
|
||||||
|
["asm"] = "keyword",
|
||||||
|
["bitand"] = "keyword",
|
||||||
|
["bitor"] = "keyword",
|
||||||
|
["catch"] = "keyword",
|
||||||
|
["throw"] = "keyword",
|
||||||
|
["try"] = "keyword",
|
||||||
|
["class"] = "keyword",
|
||||||
|
["compl"] = "keyword",
|
||||||
|
["explicit"] = "keyword",
|
||||||
|
["export"] = "keyword",
|
||||||
|
["concept"] = "keyword",
|
||||||
|
["consteval"] = "keyword",
|
||||||
|
["constexpr"] = "keyword",
|
||||||
|
["constinit"] = "keyword",
|
||||||
|
["const_cast"] = "keyword",
|
||||||
|
["dynamic_cast"] = "keyword",
|
||||||
|
["reinterpret_cast"] = "keyword",
|
||||||
|
["static_cast"] = "keyword",
|
||||||
|
["static_assert"] = "keyword",
|
||||||
|
["template"] = "keyword",
|
||||||
|
["this"] = "keyword",
|
||||||
|
["thread_local"] = "keyword",
|
||||||
|
["requires"] = "keyword",
|
||||||
|
["co_wait"] = "keyword",
|
||||||
|
["co_return"] = "keyword",
|
||||||
|
["co_yield"] = "keyword",
|
||||||
|
["decltype"] = "keyword",
|
||||||
|
["delete"] = "keyword",
|
||||||
|
["friend"] = "keyword",
|
||||||
|
["typeid"] = "keyword",
|
||||||
|
["typename"] = "keyword",
|
||||||
|
["mutable"] = "keyword",
|
||||||
|
["override"] = "keyword",
|
||||||
|
["virtual"] = "keyword",
|
||||||
|
["using"] = "keyword",
|
||||||
|
["namespace"] = "keyword",
|
||||||
|
["new"] = "keyword",
|
||||||
|
["noexcept"] = "keyword",
|
||||||
|
["if"] = "keyword",
|
||||||
|
["then"] = "keyword",
|
||||||
|
["else"] = "keyword",
|
||||||
|
["elseif"] = "keyword",
|
||||||
|
["do"] = "keyword",
|
||||||
|
["while"] = "keyword",
|
||||||
|
["for"] = "keyword",
|
||||||
|
["break"] = "keyword",
|
||||||
|
["continue"] = "keyword",
|
||||||
|
["return"] = "keyword",
|
||||||
|
["goto"] = "keyword",
|
||||||
|
["struct"] = "keyword",
|
||||||
|
["union"] = "keyword",
|
||||||
|
["typedef"] = "keyword",
|
||||||
|
["enum"] = "keyword",
|
||||||
|
["extern"] = "keyword",
|
||||||
|
["static"] = "keyword",
|
||||||
|
["volatile"] = "keyword",
|
||||||
|
["const"] = "keyword",
|
||||||
|
["inline"] = "keyword",
|
||||||
|
["case"] = "keyword",
|
||||||
|
["default"] = "keyword",
|
||||||
|
["auto"] = "keyword",
|
||||||
|
["void"] = "keyword2",
|
||||||
|
["int"] = "keyword2",
|
||||||
|
["short"] = "keyword2",
|
||||||
|
["long"] = "keyword2",
|
||||||
|
["float"] = "keyword2",
|
||||||
|
["double"] = "keyword2",
|
||||||
|
["char"] = "keyword2",
|
||||||
|
["unsigned"] = "keyword2",
|
||||||
|
["bool"] = "keyword2",
|
||||||
|
["true"] = "literal",
|
||||||
|
["false"] = "literal",
|
||||||
|
["NULL"] = "literal",
|
||||||
|
["wchar_t"] = "keyword2",
|
||||||
|
["char8_t"] = "keyword2",
|
||||||
|
["char16_t"] = "keyword2",
|
||||||
|
["char32_t"] = "keyword2",
|
||||||
|
["#include"] = "keyword",
|
||||||
|
["#if"] = "keyword",
|
||||||
|
["#ifdef"] = "keyword",
|
||||||
|
["#ifndef"] = "keyword",
|
||||||
|
["#elif"] = "keyword",
|
||||||
|
["#else"] = "keyword",
|
||||||
|
["#elseif"] = "keyword",
|
||||||
|
["#endif"] = "keyword",
|
||||||
|
["#define"] = "keyword",
|
||||||
|
["#warning"] = "keyword",
|
||||||
|
["#error"] = "keyword",
|
||||||
|
["#pragma"] = "keyword",
|
||||||
|
["end"] = "keyword",
|
||||||
|
["element"] = "keyword",
|
||||||
|
["label"] = "keyword",
|
||||||
|
["measure"] = "keyword",
|
||||||
|
["mesure"] = "keyword",
|
||||||
|
["Element"] = "keyword",
|
||||||
|
["whoinElement"] = "keyword",
|
||||||
|
["region"] = "keyword",
|
||||||
|
["R3"] = "keyword",
|
||||||
|
["vertex"] = "keyword",
|
||||||
|
["im"] = "keyword",
|
||||||
|
["l1"] = "keyword",
|
||||||
|
["l2"] = "keyword",
|
||||||
|
["linfty"] = "keyword",
|
||||||
|
["max"] = "keyword",
|
||||||
|
["min"] = "keyword",
|
||||||
|
["re"] = "keyword",
|
||||||
|
["sum"] = "keyword",
|
||||||
|
["quantile"] = "keyword",
|
||||||
|
["sort"] = "keyword",
|
||||||
|
["x"] = "keyword",
|
||||||
|
["y"] = "keyword",
|
||||||
|
["z"] = "keyword",
|
||||||
|
["length"] = "keyword",
|
||||||
|
["area"] = "keyword",
|
||||||
|
["coef"] = "keyword",
|
||||||
|
["diag"] = "keyword",
|
||||||
|
["m"] = "keyword",
|
||||||
|
["n"] = "keyword",
|
||||||
|
["nbcoef"] = "keyword",
|
||||||
|
["nnz"] = "keyword",
|
||||||
|
["resize"] = "keyword",
|
||||||
|
["size"] = "keyword",
|
||||||
|
["imax"] = "keyword",
|
||||||
|
["imin"] = "keyword",
|
||||||
|
["N"] = "keyword",
|
||||||
|
["P"] = "keyword",
|
||||||
|
["nuTriangle"] = "keyword",
|
||||||
|
["ndof"] = "keyword",
|
||||||
|
["ndofK"] = "keyword",
|
||||||
|
["nt"] = "keyword",
|
||||||
|
["be"] = "keyword",
|
||||||
|
["hmax"] = "keyword",
|
||||||
|
["hmin"] = "keyword",
|
||||||
|
["nbe"] = "keyword",
|
||||||
|
["nv"] = "keyword",
|
||||||
|
["bordermesure"] = "keyword",
|
||||||
|
["eof"] = "keyword",
|
||||||
|
["good"] = "keyword",
|
||||||
|
["fixed"] = "keyword",
|
||||||
|
["flush"] = "keyword",
|
||||||
|
["noshowbase"] = "keyword",
|
||||||
|
["noshowpos"] = "keyword",
|
||||||
|
["precision"] = "keyword",
|
||||||
|
["scientific"] = "keyword",
|
||||||
|
["seekp"] = "keyword",
|
||||||
|
["showbase"] = "keyword",
|
||||||
|
["showpos"] = "keyword",
|
||||||
|
["tellp"] = "keyword",
|
||||||
|
["ARGV"] = "keyword",
|
||||||
|
["CG"] = "keyword",
|
||||||
|
["CPUTime"] = "keyword",
|
||||||
|
["Cholesky"] = "keyword",
|
||||||
|
["Cofactor"] = "keyword",
|
||||||
|
["Crout"] = "keyword",
|
||||||
|
["Edge03d"] = "keyword",
|
||||||
|
["GMRES"] = "keyword",
|
||||||
|
["HaveUMFPACK"] = "keyword",
|
||||||
|
["LU"] = "keyword",
|
||||||
|
["NaN"] = "keyword",
|
||||||
|
["P0"] = "keyword",
|
||||||
|
["P03d"] = "keyword",
|
||||||
|
["P0VF"] = "keyword",
|
||||||
|
["P0edge"] = "keyword",
|
||||||
|
["P1"] = "keyword",
|
||||||
|
["P13d"] = "keyword",
|
||||||
|
["P1b"] = "keyword",
|
||||||
|
["P1b3d"] = "keyword",
|
||||||
|
["P1dc"] = "keyword",
|
||||||
|
["P1nc"] = "keyword",
|
||||||
|
["P2"] = "keyword",
|
||||||
|
["P23d"] = "keyword",
|
||||||
|
["P2b"] = "keyword",
|
||||||
|
["P2dc"] = "keyword",
|
||||||
|
["P2h"] = "keyword",
|
||||||
|
["RT0"] = "keyword",
|
||||||
|
["RT03d"] = "keyword",
|
||||||
|
["RT0Ortho"] = "keyword",
|
||||||
|
["RTmodif"] = "keyword",
|
||||||
|
["UMFPACK"] = "keyword",
|
||||||
|
["append"] = "keyword",
|
||||||
|
["binary"] = "keyword",
|
||||||
|
["hTriangle"] = "keyword",
|
||||||
|
["havesparsesolver"] = "keyword",
|
||||||
|
["inside"] = "keyword",
|
||||||
|
["lenEdge"] = "keyword",
|
||||||
|
["nTonEdge"] = "keyword",
|
||||||
|
["nuEdge"] = "keyword",
|
||||||
|
["pi"] = "keyword",
|
||||||
|
["qf1pE"] = "keyword",
|
||||||
|
["qf1pElump"] = "keyword",
|
||||||
|
["qf1pT"] = "keyword",
|
||||||
|
["qf1pTlump"] = "keyword",
|
||||||
|
["qf2pE"] = "keyword",
|
||||||
|
["qf2pT"] = "keyword",
|
||||||
|
["qf2pT4P1"] = "keyword",
|
||||||
|
["qf3pE"] = "keyword",
|
||||||
|
["qf4pE"] = "keyword",
|
||||||
|
["qf5pE"] = "keyword",
|
||||||
|
["qf5pT"] = "keyword",
|
||||||
|
["qf7pT"] = "keyword",
|
||||||
|
["qf9pT"] = "keyword",
|
||||||
|
["qfV1"] = "keyword",
|
||||||
|
["qfV1lump"] = "keyword",
|
||||||
|
["qfV2"] = "keyword",
|
||||||
|
["qfV5"] = "keyword",
|
||||||
|
["searchMethod"] = "keyword",
|
||||||
|
["sparsesolver"] = "keyword",
|
||||||
|
["sparsesolverSym"] = "keyword",
|
||||||
|
["storagetotal"] = "keyword",
|
||||||
|
["storageused"] = "keyword",
|
||||||
|
["verbosity"] = "keyword",
|
||||||
|
["version"] = "keyword",
|
||||||
|
["volume"] = "keyword",
|
||||||
|
["volumelevelset"] = "keyword",
|
||||||
|
["wait"] = "keyword",
|
||||||
|
["ShowAlloc"] = "keyword",
|
||||||
|
["Newton"] = "keyword",
|
||||||
|
["NoGraphicWindow"] = "keyword",
|
||||||
|
["NoUseOfWait"] = "keyword",
|
||||||
|
["SameMesh"] = "keyword",
|
||||||
|
["Unique"] = "keyword",
|
||||||
|
["arealevelset"] = "keyword",
|
||||||
|
["average"] = "keyword",
|
||||||
|
["chtmpdir"] = "keyword",
|
||||||
|
["time"] = "keyword",
|
||||||
|
["fill"] = "keyword",
|
||||||
|
["value"] = "keyword",
|
||||||
|
["nbiso"] = "keyword",
|
||||||
|
["coeff"] = "keyword",
|
||||||
|
["dataname"] = "keyword",
|
||||||
|
["order"] = "keyword",
|
||||||
|
["mpirank"] = "keyword",
|
||||||
|
["mpiCommWorld"] = "keyword",
|
||||||
|
["mpiGroup"] = "keyword",
|
||||||
|
["mpiRequest"] = "keyword",
|
||||||
|
["sparams"] = "keyword",
|
||||||
|
["mpisize"] = "keyword",
|
||||||
|
["mpiUndefined"] = "keyword",
|
||||||
|
["mpiAnySource"] = "keyword",
|
||||||
|
["communicator"] = "keyword",
|
||||||
|
["worker"] = "keyword",
|
||||||
|
["dim"] = "keyword",
|
||||||
|
["cmm"] = "keyword",
|
||||||
|
["solver"] = "keyword",
|
||||||
|
["aniso"] = "keyword",
|
||||||
|
["nbvx"] = "keyword",
|
||||||
|
["abserror"] = "keyword",
|
||||||
|
["anisomax"] = "keyword",
|
||||||
|
["cutoff"] = "keyword",
|
||||||
|
["err"] = "keyword",
|
||||||
|
["errg"] = "keyword",
|
||||||
|
["inquire"] = "keyword",
|
||||||
|
["IsMetric"] = "keyword",
|
||||||
|
["iso"] = "keyword",
|
||||||
|
["keepbackvertices"] = "keyword",
|
||||||
|
["maxsubdiv"] = "keyword",
|
||||||
|
["metric"] = "keyword",
|
||||||
|
["nbjacoby"] = "keyword",
|
||||||
|
["nbsmooth"] = "keyword",
|
||||||
|
["nomeshgeneration"] = "keyword",
|
||||||
|
["omega"] = "keyword",
|
||||||
|
["periodic"] = "keyword",
|
||||||
|
["powerin"] = "keyword",
|
||||||
|
["ratio"] = "keyword",
|
||||||
|
["rescaling"] = "keyword",
|
||||||
|
["splitin2"] = "keyword",
|
||||||
|
["splitpbedge"] = "keyword",
|
||||||
|
["thetamax"] = "keyword",
|
||||||
|
["uniform"] = "keyword",
|
||||||
|
["fixedborder"] = "keyword",
|
||||||
|
["flags"] = "keyword",
|
||||||
|
["ivalue"] = "keyword",
|
||||||
|
["maxit"] = "keyword",
|
||||||
|
["mode"] = "keyword",
|
||||||
|
["ncv"] = "keyword",
|
||||||
|
["nev"] = "keyword",
|
||||||
|
["rawvector"] = "keyword",
|
||||||
|
["sigma"] = "keyword",
|
||||||
|
["sym"] = "keyword",
|
||||||
|
["tol"] = "keyword",
|
||||||
|
["vector"] = "keyword",
|
||||||
|
["which"] = "keyword",
|
||||||
|
["op"] = "keyword",
|
||||||
|
["t"] = "keyword",
|
||||||
|
["eps"] = "keyword",
|
||||||
|
["nbiter"] = "keyword",
|
||||||
|
["precon"] = "keyword",
|
||||||
|
["veps"] = "keyword",
|
||||||
|
["tgv"] = "keyword",
|
||||||
|
["tolpivot"] = "keyword",
|
||||||
|
["meditff"] = "keyword",
|
||||||
|
["save"] = "keyword",
|
||||||
|
["orientation"] = "keyword",
|
||||||
|
["ptmerge"] = "keyword",
|
||||||
|
["transfo"] = "keyword",
|
||||||
|
["optimize"] = "keyword",
|
||||||
|
["aspectratio"] = "keyword",
|
||||||
|
["bb"] = "keyword",
|
||||||
|
["boundary"] = "keyword",
|
||||||
|
["bw"] = "keyword",
|
||||||
|
["cut"] = "keyword",
|
||||||
|
["grey"] = "keyword",
|
||||||
|
["hsv"] = "keyword",
|
||||||
|
["nbarrow"] = "keyword",
|
||||||
|
["ps"] = "keyword",
|
||||||
|
["varrow"] = "keyword",
|
||||||
|
["viso"] = "keyword",
|
||||||
|
["init"] = "keyword",
|
||||||
|
["strategy"] = "keyword",
|
||||||
|
["tolpivotsym"] = "keyword",
|
||||||
|
["facetcl"] = "keyword",
|
||||||
|
["holelist"] = "keyword",
|
||||||
|
["nboffacetcl"] = "keyword",
|
||||||
|
["nbofregions"] = "keyword",
|
||||||
|
["regionlist"] = "keyword",
|
||||||
|
["switch"] = "keyword",
|
||||||
|
["refface"] = "keyword",
|
||||||
|
["split"] = "keyword",
|
||||||
|
["zbound"] = "keyword",
|
||||||
|
["labeldown"] = "keyword",
|
||||||
|
["labelmid"] = "keyword",
|
||||||
|
["labelup"] = "keyword",
|
||||||
|
["opt"] = "keyword",
|
||||||
|
["mpiMAX"] = "keyword",
|
||||||
|
["mpiMIN"] = "keyword",
|
||||||
|
["mpiSUM"] = "keyword",
|
||||||
|
["mpiPROD"] = "keyword",
|
||||||
|
["mpiLAND"] = "keyword",
|
||||||
|
["mpiLOR"] = "keyword",
|
||||||
|
["mpiLXOR"] = "keyword",
|
||||||
|
["mpiBAND"] = "keyword",
|
||||||
|
["mpiBXOR"] = "keyword",
|
||||||
|
["border"] = "keyword2",
|
||||||
|
["Cmapmatrix"] = "keyword2",
|
||||||
|
["Cmatrix"] = "keyword2",
|
||||||
|
["complex"] = "keyword2",
|
||||||
|
["fespace"] = "keyword2",
|
||||||
|
["func"] = "keyword2",
|
||||||
|
["ifstream"] = "keyword2",
|
||||||
|
["mapmatrix"] = "keyword2",
|
||||||
|
["matrix"] = "keyword2",
|
||||||
|
["mesh"] = "keyword2",
|
||||||
|
["mesh3"] = "keyword2",
|
||||||
|
["ofstream"] = "keyword2",
|
||||||
|
["problem"] = "keyword2",
|
||||||
|
["real"] = "keyword2",
|
||||||
|
["solve"] = "keyword2",
|
||||||
|
["string"] = "keyword2",
|
||||||
|
["varf"] = "keyword2",
|
||||||
|
["macro"] = "keyword2",
|
||||||
|
["dmatrix"] = "keyword2",
|
||||||
|
["adj"] = "function",
|
||||||
|
["find"] = "function",
|
||||||
|
["rfind"] = "function",
|
||||||
|
["seekg"] = "function",
|
||||||
|
["tellg"] = "function",
|
||||||
|
["AddLayers"] = "function",
|
||||||
|
["AffineCG"] = "function",
|
||||||
|
["AffineGMRES"] = "function",
|
||||||
|
["BFGS"] = "function",
|
||||||
|
["EigenValue"] = "function",
|
||||||
|
["LinearCG"] = "function",
|
||||||
|
["LinearGMRES"] = "function",
|
||||||
|
["NLCG"] = "function",
|
||||||
|
["abs"] = "function",
|
||||||
|
["acos"] = "function",
|
||||||
|
["acosh"] = "function",
|
||||||
|
["adaptmesh"] = "function",
|
||||||
|
["arg"] = "function",
|
||||||
|
["asin"] = "function",
|
||||||
|
["asinh"] = "function",
|
||||||
|
["assert"] = "function",
|
||||||
|
["atan"] = "function",
|
||||||
|
["atan2"] = "function",
|
||||||
|
["atanh"] = "function",
|
||||||
|
["atof"] = "function",
|
||||||
|
["atoi"] = "function",
|
||||||
|
["boundingbox"] = "function",
|
||||||
|
["buildmesh"] = "function",
|
||||||
|
["buildmeshborder"] = "function",
|
||||||
|
["ceil"] = "function",
|
||||||
|
["change"] = "function",
|
||||||
|
["checkmovemesh"] = "function",
|
||||||
|
["clock"] = "function",
|
||||||
|
["complexEigenValue"] = "function",
|
||||||
|
["conj"] = "function",
|
||||||
|
["convect"] = "function",
|
||||||
|
["cos"] = "function",
|
||||||
|
["cosh"] = "function",
|
||||||
|
["defaultoUMFPACK"] = "function",
|
||||||
|
["defaultsolver"] = "function",
|
||||||
|
["defaulttoCG"] = "function",
|
||||||
|
["defaulttoGMRES"] = "function",
|
||||||
|
["defaulttoUMFPACK"] = "function",
|
||||||
|
["det"] = "function",
|
||||||
|
["dumptable"] = "function",
|
||||||
|
["dx"] = "function",
|
||||||
|
["dxx"] = "function",
|
||||||
|
["dxy"] = "function",
|
||||||
|
["dxz"] = "function",
|
||||||
|
["dy"] = "function",
|
||||||
|
["dyx"] = "function",
|
||||||
|
["dyy"] = "function",
|
||||||
|
["dyz"] = "function",
|
||||||
|
["dz"] = "function",
|
||||||
|
["dzx"] = "function",
|
||||||
|
["dzy"] = "function",
|
||||||
|
["dzz"] = "function",
|
||||||
|
["emptymesh"] = "function",
|
||||||
|
["erf"] = "function",
|
||||||
|
["erfc"] = "function",
|
||||||
|
["exec"] = "function",
|
||||||
|
["exit"] = "function",
|
||||||
|
["exp"] = "function",
|
||||||
|
["floor"] = "function",
|
||||||
|
["getline"] = "function",
|
||||||
|
["hypot"] = "function",
|
||||||
|
["imag"] = "function",
|
||||||
|
["int1d"] = "function",
|
||||||
|
["int2d"] = "function",
|
||||||
|
["int3d"] = "function",
|
||||||
|
["intallVFedges"] = "function",
|
||||||
|
["intalledges"] = "function",
|
||||||
|
["intallfaces"] = "function",
|
||||||
|
["interplotematrix"] = "function",
|
||||||
|
["interpolate"] = "function",
|
||||||
|
["isInf"] = "function",
|
||||||
|
["isNaN"] = "function",
|
||||||
|
["isNormal"] = "function",
|
||||||
|
["j0"] = "function",
|
||||||
|
["j1"] = "function",
|
||||||
|
["jn"] = "function",
|
||||||
|
["jump"] = "function",
|
||||||
|
["lgamma"] = "function",
|
||||||
|
["log"] = "function",
|
||||||
|
["log10"] = "function",
|
||||||
|
["lrint"] = "function",
|
||||||
|
["lround"] = "function",
|
||||||
|
["ltime"] = "function",
|
||||||
|
["mean"] = "function",
|
||||||
|
["movemesh"] = "function",
|
||||||
|
["newconvect"] = "function",
|
||||||
|
["norm"] = "function",
|
||||||
|
["on"] = "function",
|
||||||
|
["otherside"] = "function",
|
||||||
|
["plot"] = "function",
|
||||||
|
["polar"] = "function",
|
||||||
|
["pow"] = "function",
|
||||||
|
["randinit"] = "function",
|
||||||
|
["randint31"] = "function",
|
||||||
|
["randint32"] = "function",
|
||||||
|
["randreal1"] = "function",
|
||||||
|
["randreal2"] = "function",
|
||||||
|
["randreal3"] = "function",
|
||||||
|
["randres53"] = "function",
|
||||||
|
["readmesh"] = "function",
|
||||||
|
["readmesh3"] = "function",
|
||||||
|
["renumbering"] = "function",
|
||||||
|
["restrict"] = "function",
|
||||||
|
["rint"] = "function",
|
||||||
|
["round"] = "function",
|
||||||
|
["savemesh"] = "function",
|
||||||
|
["savesurfacemesh"] = "function",
|
||||||
|
["set"] = "function",
|
||||||
|
["setw"] = "function",
|
||||||
|
["showCPU"] = "function",
|
||||||
|
["sin"] = "function",
|
||||||
|
["sinh"] = "function",
|
||||||
|
["splitmesh"] = "function",
|
||||||
|
["sqr"] = "function",
|
||||||
|
["sqrt"] = "function",
|
||||||
|
["square"] = "function",
|
||||||
|
["system"] = "function",
|
||||||
|
["tan"] = "function",
|
||||||
|
["tanh"] = "function",
|
||||||
|
["tgamma"] = "function",
|
||||||
|
["toCarray"] = "function",
|
||||||
|
["toRarray"] = "function",
|
||||||
|
["toZarray"] = "function",
|
||||||
|
["trace"] = "function",
|
||||||
|
["triangulate"] = "function",
|
||||||
|
["trunc"] = "function",
|
||||||
|
["y0"] = "function",
|
||||||
|
["y1"] = "function",
|
||||||
|
["yn"] = "function",
|
||||||
|
["savevtk"] = "function",
|
||||||
|
["mshmet"] = "function",
|
||||||
|
["savesol"] = "function",
|
||||||
|
["gmshload"] = "function",
|
||||||
|
["gmshload3"] = "function",
|
||||||
|
["mpiBarrier"] = "function",
|
||||||
|
["mpiSize"] = "function",
|
||||||
|
["Irecv"] = "function",
|
||||||
|
["Isend"] = "function",
|
||||||
|
["processor"] = "function",
|
||||||
|
["mpiWaitAny"] = "function",
|
||||||
|
["mpiWait"] = "function",
|
||||||
|
["mpiRank"] = "function",
|
||||||
|
["metis"] = "function",
|
||||||
|
["metisdual"] = "function",
|
||||||
|
["broadcast"] = "function",
|
||||||
|
["scotch"] = "function",
|
||||||
|
["parmetis"] = "function",
|
||||||
|
["mpiWtime"] = "function",
|
||||||
|
["buildlayers"] = "function",
|
||||||
|
["mmg3d"] = "function",
|
||||||
|
["processorblock"] = "function",
|
||||||
|
["mpiWaitAll"] = "function",
|
||||||
|
["mpiWtick"] = "function",
|
||||||
|
["Send"] = "function",
|
||||||
|
["Recv"] = "function",
|
||||||
|
["mpiAlltoall"] = "function",
|
||||||
|
["mpiGather"] = "function",
|
||||||
|
["mpiScatter"] = "function",
|
||||||
|
["mpiReduce"] = "function",
|
||||||
|
["mpiAllReduce"] = "function",
|
||||||
|
["mpiReduceScatter"] = "function",
|
||||||
|
},
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user