Compare commits

16 Commits
Author SHA1 Message Date
vhaudiquet 56ef7344f0 Vendor selectionhighlight, bracketmatch, colorpreview, navigate, editorconfig (2.1.8-7)
Five more plugins from lite-xl-plugins @ 31eef29, all default VSCode
behaviors:

- selectionhighlight: highlights other occurrences of the selected word
- bracketmatch: highlights the bracket matching the one under the cursor
- colorpreview: inline swatches for #hex/rgb()/hsl() color literals
- navigate: Go Back / Go Forward cursor history (alt+left/right)
- editorconfig: applies .editorconfig files; vendored as the whole
  upstream directory (parser + test harness, lite-xl test editorconfig);
  builds on the bundled trimwhitespace plugin

All depend on core only (editorconfig additionally on trimwhitespace,
already shipped). Verified against the built package: all plugins load,
navigate registers its commands, and an .editorconfig with
indent_size = 2 is applied to an opened file.

Pinned to lite-xl-plugins @ 31eef29 (see debian/extra/README.source).
2026-09-17 11:18:20 +02:00
vhaudiquet 273965ef0b Vendor ephemeral_tabs (2.1.8-6)
VSCode-style preview tabs: files opened from the treeview with a single
click open in a preview tab (titled "~ name ~") that is reused for the
next file opened and becomes a permanent tab as soon as it is edited,
double-clicked or dragged.

Verified against the running editor: single-click opens preview, opening
another file replaces it, editing promotes it to a permanent tab.

Pinned to lite-xl-plugins @ 31eef29 (see debian/extra/README.source).
2026-09-17 11:03:42 +02:00
vhaudiquet 597d8a42f6 Vendor gitpanel 0.3.0 (2.1.8-5)
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. History is pinned to
the bottom half of the panel; commit patches open with diff syntax
highlighting from the already bundled language_diff.

Pinned to lite-xl-gitpanel @ 0731a63 (see debian/extra/README.source).
2026-09-17 01:45:03 +02:00
vhaudiquet b8feb4f4db Fix vendored nerdicons rendering; keep status bar always visible
- the vendored nerdicons.lua was the pristine upstream copy, so installs
  relying on the packaged plugin lost the tuned icon rendering (fractional
  glyph size and default hinting made icons feel soft and uneven). Apply
  the same tuning as the user config: whole-pixel icon size, hinting off,
  scale-aware chevron offset.
- default config (skel + first-run template) and this repo's users keep
  the status bar permanently visible: shown at startup, and the
  status-bar:hide/toggle commands are no-ops.
2026-09-16 23:02:28 +02:00
vhaudiquet 3bae6fd448 Bundle curated community plugins (2.1.8-4)
Vendored from upstream, pinned by commit (see debian/extra/README.source):
- gitstatus, gitopen, fontconfig, open_ext, select_colorscheme,
  smoothcaret, nerdicons (font mapping patched to DATADIR, Ships the
  Symbols Nerd Font Mono TTF, SIL OFL 1.1)
- lite-xl-lsp (complete LSP client; idle until a language server is
  configured) plus the lite-xl-widgets library it requires
- 104 language syntax plugins (none duplicate the nine bundled in core)

settings GUI left out upstream is broken: it requires a
libraries.widget.fonts widget that does not exist in
lite-xl-widgets at any revision.

Co-developed-with: ZCode (Z.ai)
2026-09-16 22:45:02 +02:00
vhaudiquet a693cb645c Ship out-of-the-box VSCode-style config and sharp rendering defaults
- quilt patch: first-run user init.lua template now writes a tuned
  default config (Dark Modern theme, JetBrains Mono with grayscale AA,
  line-height/indent guides, compact treeview with hover highlight)
- new theme module usr/share/lite-xl/colors/vscode_dark.lua
- default config in /etc/skel/.config/lite-xl/ for new users
- desktop: launch via SDL software renderer with LITE_SCALE=1.5 so text
  renders 1:1 on fractional-scaled Wayland outputs and avoids panfrost
  GPU artifacts
- depend on fonts-jetbrains-mono for the default code font
2026-09-16 21:52:46 +02:00
vhaudiquet ad7256b4fb debian: initial packaging work 2026-09-16 21:43:25 +02:00
takase1121 0af4dae0e2 chore: bump versions [skip ci] 2025-06-09 06:45:20 +08:00
takase1121 b5ee18c966 chore: update changelogs [skip ci] 2025-06-09 06:45:20 +08:00
takase1121 85c2c3fdc8 main: keep PLATFORM consistent between SDL2 and SDL3 2025-06-05 11:03:45 +08:00
takase1121 b5317c5318 core: make project_dir detection more robust
On macOS, listing / can silently fail due to SIP. We try to default to ~ if there's no choice.
2025-06-05 11:03:45 +08:00
takase1121 7a2214ae0e main: fix not receiving text input 2025-05-19 08:52:43 +08:00
takase1121 94b2b28377 subprojects: fix freetype2 bzip2 dep issue 2025-05-19 08:52:41 +08:00
takase1121 409d2cd00b main: set the correct SDL_HINT_IME_IMPLEMENTED_UI 2025-05-19 08:47:47 +08:00
takase1121 fceb773d96 build.sh: update SDL3 version 2025-05-19 08:47:47 +08:00
takase1121 ceea55e97d appstream: fix screenshot link 2025-05-18 23:22:57 +08:00
216 changed files with 46768 additions and 35 deletions
+4
View File
@@ -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
+1 -1
View File
@@ -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:
+3
View File
@@ -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
+87
View File
@@ -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,
+35 -20
View File
@@ -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 end
update_recents_project("add", project_dir_abs) for _, p in ipairs(try_project_dirs) do
end local abs_dir = system.absolute_path(p)
else if abs_dir and core.set_project_dir(abs_dir, load_project_module) then
if not project_dir_explicit then if p == project_dir and project_dir_explicit then
update_recents_project("remove", project_dir) update_recents_project("add", abs_dir)
end end
project_dir_abs = system.absolute_path(".") project_dir_abs = abs_dir
if not core.set_project_dir(project_dir_abs, function() break
got_project_error = not core.load_project_module() else
end) then if p == project_dir and not project_dir_explicit then
system.show_fatal_error("Lite XL internal error", "cannot set project directory to cwd") update_recents_project("remove", p)
os.exit(1) end
end end
end end
if not project_dir_abs then
system.show_fatal_error("Error", "Please restart Lite XL with a readable project directory.")
os.exit(1)
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()
+6
View File
@@ -0,0 +1,6 @@
debian/files
debian/.debhelper/
debian/*.log
debian/lite-xl/
debian/debhelper-build-stamp
debian/*.substvars
+75
View File
@@ -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
+22
View File
@@ -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.
+11
View File
@@ -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>.
+27
View File
@@ -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.
+80
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+77
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
File diff suppressed because it is too large Load Diff
+88
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+71
View File
@@ -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
+241
View File
@@ -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
+12
View File
@@ -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
View File
@@ -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
+16
View File
@@ -0,0 +1,16 @@
[*.a]
option1=value1
; repeat section
[*.a]
option2=value2
[*.b]
option1 = a
option2 = a
[b.b]
option2 = b
[*.b]
option1 = c
+6
View File
@@ -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
+107
View File
@@ -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
View File
@@ -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
+118
View File
@@ -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
+51
View File
@@ -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,
})
+678
View File
@@ -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)
+155
View File
@@ -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
})
+39
View File
@@ -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
View File
@@ -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
View File
@@ -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",
},
}
File diff suppressed because it is too large Load Diff
+155
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
}
+80
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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",
}
}
+135
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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