experimental

This commit is contained in:
Jonathan Apodaca 2025-06-12 22:48:17 -06:00
parent 7f85848620
commit 9054f4453f
7 changed files with 283 additions and 62 deletions

View File

@ -19,3 +19,6 @@ test:
@echo "## Generating coverage report" @echo "## Generating coverage report"
@luacov @luacov
@awk '/^Summary$$/{flag=1;next} flag{print}' luacov.report.out @awk '/^Summary$$/{flag=1;next} flag{print}' luacov.report.out
watch:
@watchexec -c -e lua make

View File

@ -22,10 +22,29 @@ tracker.create_effect(function()
-- constructed with `h(...)` calls). To help organize the markup, text and -- constructed with `h(...)` calls). To help organize the markup, text and
-- tags can be nested in tables at any depth. Line breaks must be specified -- tags can be nested in tables at any depth. Line breaks must be specified
-- manually, with '\n'. -- manually, with '\n'.
local text_ref = ui_buf.renderer:create_ref()
ui_buf:render { ui_buf:render {
'Reactive Counter Example\n', 'Reactive Counter Example\n',
'========================\n\n', '========================\n\n',
{
'Text field: ',
h('text', { hl = 'DiffAdd', ref = text_ref }, { '[]' }),
'\n',
h('text', {
hl = 'DiffAdd',
nmap = {
['<CR>'] = function()
vim.notify(text_ref:text())
return ''
end,
},
}, ' Submit '),
},
'\n',
'\n',
{ 'Counter: ', tostring(count), '\n' }, { 'Counter: ', tostring(count), '\n' },
'\n', '\n',

View File

@ -5,7 +5,7 @@ local Renderer = require('u.renderer').Renderer
--- @field bufnr number --- @field bufnr number
--- @field b vim.var_accessor --- @field b vim.var_accessor
--- @field bo vim.bo --- @field bo vim.bo
--- @field private renderer u.Renderer --- @field renderer u.renderer.Renderer
local Buffer = {} local Buffer = {}
Buffer.__index = Buffer Buffer.__index = Buffer

View File

@ -91,14 +91,12 @@ function Range.from_marks(lpos, rpos)
end end
--- @param bufnr number --- @param bufnr number
--- @param id number --- @param extmark vim.api.keyset.get_extmark_item_by_id
function Range.from_extmark(bufnr, id) function Range.from_extmark(bufnr, extmark)
local mode = 'v'
---@type integer, integer, vim.api.keyset.extmark_details | nil ---@type integer, integer, vim.api.keyset.extmark_details | nil
local start_row0, start_col0, details = local start_row0, start_col0, details = unpack(extmark)
unpack(vim.api.nvim_buf_get_extmark_by_id(bufnr, NS, id, { details = true }))
local mode = 'v'
local start = Pos.new(bufnr, start_row0 + 1, start_col0 + 1) local start = Pos.new(bufnr, start_row0 + 1, start_col0 + 1)
local stop = details and Pos.new(bufnr, details.end_row + 1, details.end_col) local stop = details and Pos.new(bufnr, details.end_row + 1, details.end_col)
@ -651,7 +649,14 @@ end
function ExtmarkRange.new(bufnr, id) return setmetatable({ bufnr = bufnr, id = id }, ExtmarkRange) end function ExtmarkRange.new(bufnr, id) return setmetatable({ bufnr = bufnr, id = id }, ExtmarkRange) end
function ExtmarkRange:range() return Range.from_extmark(self.bufnr, self.id) end function ExtmarkRange:range()
return Range.from_extmark(
self.bufnr,
vim.api.nvim_buf_get_extmark_by_id(self.bufnr, NS, self.id, {
details = true,
})
)
end
function ExtmarkRange:delete() vim.api.nvim_buf_del_extmark(self.bufnr, NS, self.id) end function ExtmarkRange:delete() vim.api.nvim_buf_del_extmark(self.bufnr, NS, self.id) end

View File

@ -1,12 +1,21 @@
local M = {} local M = {}
local H = {} local H = {}
--- @alias u.renderer.Tag { kind: 'tag'; name: string, attributes: table<string, unknown>, children: u.renderer.Tree } --- @alias u.renderer.TagEventHandler fun(tag: u.renderer.Tag, mode: string, lhs: string): string
--- @alias u.renderer.TagAttributes { [string]?: unknown; imap?: table<string, u.renderer.TagEventHandler>; nmap?: table<string, u.renderer.TagEventHandler>; vmap?: table<string, u.renderer.TagEventHandler>; xmap?: table<string, u.renderer.TagEventHandler>; omap?: table<string, u.renderer.TagEventHandler> }
--- @class u.renderer.Tag
--- @field kind 'tag'
--- @field name string
--- @field attributes u.renderer.TagAttributes
--- @field children u.renderer.Tree
--- @alias u.renderer.Node nil | boolean | string | u.renderer.Tag --- @alias u.renderer.Node nil | boolean | string | u.renderer.Tag
--- @alias u.renderer.Tree u.renderer.Node | u.renderer.Node[] --- @alias u.renderer.Tree u.renderer.Node | u.renderer.Node[]
-- luacheck: ignore -- luacheck: ignore
--- @type table<string, fun(attributes: table<string, any>, children: u.renderer.Tree): u.renderer.Tag> & fun(name: string, attributes: table<string, any>, children: u.renderer.Tree): u.renderer.Tag> --- @type table<string, fun(attributes: u.renderer.TagAttributes, children: u.renderer.Tree): u.renderer.Tag> & fun(name: string, attributes: u.renderer.TagAttributes, children: u.renderer.Tree): u.renderer.Tag>
M.h = setmetatable({}, { M.h = setmetatable({}, {
__call = function(_, name, attributes, children) __call = function(_, name, attributes, children)
return { return {
@ -24,15 +33,66 @@ M.h = setmetatable({}, {
end, end,
}) })
-- Renderer {{{ -- TagRef {{{
--- @alias RendererExtmark { id?: number; start: [number, number]; stop: [number, number]; opts: any; tag: any } --- @class u.renderer.TagRef
--- @field tag? u.renderer.Tag
--- @field renderer u.renderer.Renderer
local TagRef = {}
TagRef.__index = TagRef
--- @class u.Renderer function TagRef:extmark()
if not self.tag then return {} end
--- @type u.renderer.RendererExtmark?
local extmark_inf = vim
.iter(self.renderer.curr.extmarks)
--- @param x u.renderer.RendererExtmark
:find(function(x) return x.tag == self.tag end)
if not extmark_inf then return {} end
local extmark = vim.api.nvim_buf_get_extmark_by_id(
self.renderer.bufnr,
self.renderer.ns,
extmark_inf.id,
{ details = true }
)
return extmark
end
function TagRef:range()
local extmark = self:extmark()
if not extmark then return nil end
local Range = require 'u.range'
return Range.from_extmark(self.renderer.bufnr, extmark)
end
function TagRef:lines()
local range = self:range()
if not range then return {} end
return range:lines()
end
function TagRef:text()
local range = self:range()
if not range then return '' end
return range:text()
end
-- }}}
-- Renderer {{{
--- @class u.renderer.RendererExtmark
--- @field id? number
--- @field start [number, number]
--- @field stop [number, number]
--- @field opts vim.api.keyset.set_extmark
--- @field tag u.renderer.Tag
--- @class u.renderer.Renderer
--- @field bufnr number --- @field bufnr number
--- @field ns number --- @field ns number
--- @field changedtick number --- @field changedtick number
--- @field old { lines: string[]; extmarks: RendererExtmark[] } --- @field old { lines: string[]; extmarks: u.renderer.RendererExtmark[] }
--- @field curr { lines: string[]; extmarks: RendererExtmark[] } --- @field curr { lines: string[]; extmarks: u.renderer.RendererExtmark[] }
local Renderer = {} local Renderer = {}
Renderer.__index = Renderer Renderer.__index = Renderer
M.Renderer = Renderer M.Renderer = Renderer
@ -67,6 +127,8 @@ function Renderer.new(bufnr) -- {{{
return self return self
end -- }}} end -- }}}
function Renderer:create_ref() return setmetatable({ renderer = self }, TagRef) end
--- @param opts { --- @param opts {
--- tree: u.renderer.Tree; --- tree: u.renderer.Tree;
--- on_tag?: fun(tag: u.renderer.Tag, start0: [number, number], stop0: [number, number]): any; --- on_tag?: fun(tag: u.renderer.Tag, start0: [number, number], stop0: [number, number]): any;
@ -199,7 +261,7 @@ function Renderer:render(tree) -- {{{
self.changedtick = changedtick self.changedtick = changedtick
end end
--- @type RendererExtmark[] --- @type u.renderer.RendererExtmark[]
local extmarks = {} local extmarks = {}
--- @type string[] --- @type string[]
@ -214,28 +276,36 @@ function Renderer:render(tree) -- {{{
tag.attributes.extmark.hl_group = tag.attributes.extmark.hl_group or hl tag.attributes.extmark.hl_group = tag.attributes.extmark.hl_group or hl
end end
local extmark = tag.attributes.extmark local extmark_opts = tag.attributes.extmark
-- Set any necessary keymaps: -- Set any necessary keymaps:
for _, mode in ipairs { 'i', 'n', 'v', 'x', 'o' } do for _, mode in ipairs { 'i', 'n', 'v', 'x', 'o' } do
for lhs, _ in pairs(tag.attributes[mode .. 'map'] or {}) do for lhs, _ in pairs(tag.attributes[mode .. 'map'] or {}) do
-- Force creating an extmark if there are key handlers. To accurately -- Force creating an extmark if there are key handlers. To accurately
-- sense the bounds of the text, we need an extmark: -- sense the bounds of the text, we need an extmark:
extmark = extmark or {} extmark_opts = extmark_opts or {}
vim.keymap.set( vim.keymap.set(
'n', mode,
lhs, lhs,
function() return self:_expr_map_callback('n', lhs) end, function() return self:_expr_map_callback(mode, lhs) end,
{ buffer = self.bufnr, expr = true, replace_keycodes = true } { buffer = self.bufnr, expr = true, replace_keycodes = true }
) )
end end
end end
if extmark then -- Check for refs:
if tag.attributes.ref and getmetatable(tag.attributes.ref) == TagRef then
--- @type u.renderer.TagRef
local ref = tag.attributes.ref
ref.tag = tag
extmark_opts = extmark_opts or {}
end
if extmark_opts then
table.insert(extmarks, { table.insert(extmarks, {
start = start0, start = start0,
stop = stop0, stop = stop0,
opts = extmark, opts = extmark_opts,
tag = tag, tag = tag,
}) })
end end
@ -316,9 +386,10 @@ function Renderer:_expr_map_callback(mode, lhs) -- {{{
local tag = pos_info.tag local tag = pos_info.tag
-- is the tag listening? -- is the tag listening?
--- @type u.renderer.TagEventHandler?
local f = vim.tbl_get(tag.attributes, mode .. 'map', lhs) local f = vim.tbl_get(tag.attributes, mode .. 'map', lhs)
if type(f) == 'function' then if type(f) == 'function' then
local result = f() local result = f(tag, mode, lhs)
if result == '' then if result == '' then
-- bubble-up to the next tag, but set cancel to true, in case there are -- bubble-up to the next tag, but set cancel to true, in case there are
-- no more tags to bubble up to: -- no more tags to bubble up to:
@ -339,14 +410,14 @@ end -- }}}
--- ---
--- @private (private for now) --- @private (private for now)
--- @param pos0 [number; number] --- @param pos0 [number; number]
--- @return { extmark: RendererExtmark; tag: u.renderer.Tag; }[] --- @return { extmark: u.renderer.RendererExtmark; tag: u.renderer.Tag; }[]
function Renderer:get_pos_infos(pos0) -- {{{ function Renderer:get_pos_infos(pos0) -- {{{
local cursor_line0, cursor_col0 = pos0[1], pos0[2] local cursor_line0, cursor_col0 = pos0[1], pos0[2]
-- The cursor (block) occupies **two** extmark spaces: one for it's left -- The cursor (block) occupies **two** extmark spaces: one for it's left
-- edge, and one for it's right. We need to do our own intersection test, -- edge, and one for it's right. We need to do our own intersection test,
-- because the NeoVim API is over-inclusive in what it returns: -- because the NeoVim API is over-inclusive in what it returns:
--- @type RendererExtmark[] --- @type u.renderer.RendererExtmark[]
local intersecting_extmarks = vim local intersecting_extmarks = vim
.iter( .iter(
vim.api.nvim_buf_get_extmarks( vim.api.nvim_buf_get_extmarks(
@ -357,7 +428,7 @@ function Renderer:get_pos_infos(pos0) -- {{{
{ details = true, overlap = true } { details = true, overlap = true }
) )
) )
--- @return RendererExtmark --- @return u.renderer.RendererExtmark
:map(function(ext) :map(function(ext)
--- @type number, number, number, { end_row?: number; end_col?: number }|nil --- @type number, number, number, { end_row?: number; end_col?: number }|nil
local id, line0, col0, details = unpack(ext) local id, line0, col0, details = unpack(ext)
@ -368,7 +439,7 @@ function Renderer:get_pos_infos(pos0) -- {{{
end end
return { id = id, start = start, stop = stop, opts = details } return { id = id, start = start, stop = stop, opts = details }
end) end)
--- @param ext RendererExtmark --- @param ext u.renderer.RendererExtmark
:filter(function(ext) :filter(function(ext)
if ext.stop[1] ~= nil and ext.stop[2] ~= nil then if ext.stop[1] ~= nil and ext.stop[2] ~= nil then
return cursor_line0 >= ext.start[1] return cursor_line0 >= ext.start[1]
@ -384,8 +455,8 @@ function Renderer:get_pos_infos(pos0) -- {{{
-- Sort the tags into smallest (inner) to largest (outer): -- Sort the tags into smallest (inner) to largest (outer):
table.sort( table.sort(
intersecting_extmarks, intersecting_extmarks,
--- @param x1 RendererExtmark --- @param x1 u.renderer.RendererExtmark
--- @param x2 RendererExtmark --- @param x2 u.renderer.RendererExtmark
function(x1, x2) function(x1, x2)
if if
x1.start[1] == x2.start[1] x1.start[1] == x2.start[1]
@ -407,10 +478,10 @@ function Renderer:get_pos_infos(pos0) -- {{{
-- created extmarks in self.curr.extmarks, which also has which tag each -- created extmarks in self.curr.extmarks, which also has which tag each
-- extmark is associated with. Cross-reference with that list to get a list -- extmark is associated with. Cross-reference with that list to get a list
-- of tags that we need to fire events for: -- of tags that we need to fire events for:
--- @type { extmark: RendererExtmark; tag: u.renderer.Tag }[] --- @type { extmark: u.renderer.RendererExtmark; tag: u.renderer.Tag }[]
local matching_tags = vim local matching_tags = vim
.iter(intersecting_extmarks) .iter(intersecting_extmarks)
--- @param ext RendererExtmark --- @param ext u.renderer.RendererExtmark
:map(function(ext) :map(function(ext)
for _, extmark_cache in ipairs(self.curr.extmarks) do for _, extmark_cache in ipairs(self.curr.extmarks) do
if extmark_cache.id == ext.id then return { extmark = ext, tag = extmark_cache.tag } end if extmark_cache.id == ext.id then return { extmark = ext, tag = extmark_cache.tag } end
@ -423,7 +494,7 @@ end -- }}}
-- }}} -- }}}
-- TreeBuilder {{{ -- TreeBuilder {{{
--- @class u.TreeBuilder --- @class u.renderer.TreeBuilder
--- @field private nodes u.renderer.Node[] --- @field private nodes u.renderer.Node[]
local TreeBuilder = {} local TreeBuilder = {}
TreeBuilder.__index = TreeBuilder TreeBuilder.__index = TreeBuilder
@ -435,7 +506,7 @@ function TreeBuilder.new()
end end
--- @param nodes u.renderer.Tree --- @param nodes u.renderer.Tree
--- @return u.TreeBuilder --- @return u.renderer.TreeBuilder
function TreeBuilder:put(nodes) function TreeBuilder:put(nodes)
table.insert(self.nodes, nodes) table.insert(self.nodes, nodes)
return self return self
@ -444,7 +515,7 @@ end
--- @param name string --- @param name string
--- @param attributes? table<string, any> --- @param attributes? table<string, any>
--- @param children? u.renderer.Node | u.renderer.Node[] --- @param children? u.renderer.Node | u.renderer.Node[]
--- @return u.TreeBuilder --- @return u.renderer.TreeBuilder
function TreeBuilder:put_h(name, attributes, children) function TreeBuilder:put_h(name, attributes, children)
local tag = M.h(name, attributes, children) local tag = M.h(name, attributes, children)
table.insert(self.nodes, tag) table.insert(self.nodes, tag)
@ -452,7 +523,7 @@ function TreeBuilder:put_h(name, attributes, children)
end end
--- @param fn fun(TreeBuilder): any --- @param fn fun(TreeBuilder): any
--- @return u.TreeBuilder --- @return u.renderer.TreeBuilder
function TreeBuilder:nest(fn) function TreeBuilder:nest(fn)
local nested_writer = TreeBuilder.new() local nested_writer = TreeBuilder.new()
fn(nested_writer) fn(nested_writer)

View File

@ -4,17 +4,63 @@ local M = {}
-- Types -- Types
-- --
--- @alias QfItem { col: number, filename: string, kind: string, lnum: number, text: string } --- @class u.utils.QfItem
--- @alias KeyMaps table<string, fun(): any | string> } --- @field col number
-- luacheck: ignore --- @field filename string
--- @alias RawCmdArgs { args: string; bang: boolean; count: number; fargs: string[]; line1: number; line2: number; mods: string; name: string; range: 0|1|2; reg: string; smods: any } --- @field kind string
-- luacheck: ignore --- @field lnum number
--- @alias CmdArgs { args: string; bang: boolean; count: number; fargs: string[]; line1: number; line2: number; mods: string; name: string; range: 0|1|2; reg: string; smods: any; info: u.Range|nil } --- @field text string
--- @class u.utils.RawCmdArgs
--- @field args string
--- @field bang boolean
--- @field count number
--- @field fargs string[]
--- @field line1 number
--- @field line2 number
--- @field mods string
--- @field name string
--- @field range 0|1|2
--- @field reg string
--- @field smods any
--- @class u.utils.CmdArgs: u.utils.RawCmdArgs
--- @field info u.Range|nil
--- @class u.utils.UcmdArgs
--- @field nargs? 0|1|'*'|'?'|'+'
--- @field range? boolean|'%'|number
--- @field count? boolean|number
--- @field addr? string
--- @field completion? string
--- @field force? boolean
--- @field preview? fun(opts: u.utils.UcmdArgs, ns: integer, buf: integer):0|1|2
--
-- Functions
--
--- Debug utility that prints a value and returns it unchanged.
--- Useful for debugging in the middle of expressions or function chains.
---
--- @generic T --- @generic T
--- @param x `T` --- @param x `T` The value to debug print
--- @param message? string --- @param message? string Optional message to print alongside the value
--- @return T --- @return T The original value, unchanged
---
--- @usage
--- ```lua
--- -- Debug a value in the middle of a chain:
--- local result = some_function()
--- :map(utils.dbg) -- prints the intermediate value
--- :filter(predicate)
---
--- -- Debug with a custom message:
--- local config = utils.dbg(get_config(), "Current config:")
---
--- -- Debug return values:
--- return utils.dbg(calculate_result(), "Final result")
--- ```
function M.dbg(x, message) function M.dbg(x, message)
local t = {} local t = {}
if message ~= nil then table.insert(t, message) end if message ~= nil then table.insert(t, message) end
@ -23,22 +69,37 @@ function M.dbg(x, message)
return x return x
end end
--- A utility for creating user commands that also pre-computes useful information --- Creates a user command with enhanced argument processing.
--- and attaches it to the arguments. --- Automatically computes range information and attaches it as `args.info`.
--- ---
--- @param name string The command name (without the leading colon)
--- @param cmd string | fun(args: u.utils.CmdArgs): any Command implementation
--- @param opts? u.utils.UcmdArgs Command options (nargs, range, etc.)
---
--- @usage
--- ```lua --- ```lua
--- -- Example: --- -- Create a command that works with visual selections:
--- ucmd('MyCmd', function(args) --- utils.ucmd('MyCmd', function(args)
--- -- print the visually selected text: --- -- Print the visually selected text:
--- vim.print(args.info:text()) --- vim.print(args.info:text())
--- -- or get the vtext as an array of lines: --- -- Or get the selection as an array of lines:
--- vim.print(args.info:lines()) --- vim.print(args.info:lines())
--- end, { nargs = '*', range = true }) --- end, { nargs = '*', range = true })
---
--- -- Create a command that processes the current line:
--- utils.ucmd('ProcessLine', function(args)
--- local line_text = args.info:text()
--- -- Process the line...
--- end, { range = '%' })
---
--- -- Create a command with arguments:
--- utils.ucmd('SearchReplace', function(args)
--- local pattern, replacement = args.fargs[1], args.fargs[2]
--- local text = args.info:text()
--- -- Perform search and replace...
--- end, { nargs = 2, range = true })
--- ``` --- ```
--- @param name string
--- @param cmd string | fun(args: CmdArgs): any
-- luacheck: ignore -- luacheck: ignore
--- @param opts? { nargs?: 0|1|'*'|'?'|'+'; range?: boolean|'%'|number; count?: boolean|number, addr?: string; completion?: string }
function M.ucmd(name, cmd, opts) function M.ucmd(name, cmd, opts)
local Range = require 'u.range' local Range = require 'u.range'
@ -50,20 +111,81 @@ function M.ucmd(name, cmd, opts)
return cmd(args) return cmd(args)
end end
end end
vim.api.nvim_create_user_command(name, cmd2, opts or {}) vim.api.nvim_create_user_command(name, cmd2, opts or {} --[[@as any]])
end end
--- @param current_args RawCmdArgs --- Creates command arguments for delegating from one command to another.
function M.create_forward_cmd_args(current_args) --- Preserves all relevant context (range, modifiers, bang, etc.) when
local args = { args = current_args.fargs } --- implementing a derived command in terms of a base command.
if current_args.range == 1 then ---
args.range = { current_args.line1 } --- @param current_args vim.api.keyset.create_user_command.command_args|u.utils.RawCmdArgs The arguments from the current command
elseif current_args.range == 2 then --- @return vim.api.keyset.cmd Arguments suitable for vim.cmd() calls
args.range = { current_args.line1, current_args.line2 } ---
end --- @usage
--- ```lua
--- -- Implement :MyEdit in terms of :edit, preserving all context:
--- utils.ucmd('MyEdit', function(args)
--- local delegated_args = utils.create_delegated_cmd_args(args)
--- -- Add custom logic here...
--- vim.cmd.edit(delegated_args)
--- end, { nargs = '*', range = true, bang = true })
---
--- -- Implement :MySubstitute that delegates to :substitute:
--- utils.ucmd('MySubstitute', function(args)
--- -- Pre-process arguments
--- local pattern = preprocess_pattern(args.fargs[1])
---
--- local delegated_args = utils.create_delegated_cmd_args(args)
--- delegated_args.args = { pattern, args.fargs[2] }
---
--- vim.cmd.substitute(delegated_args)
--- end, { nargs = 2, range = true, bang = true })
--- ```
function M.create_delegated_cmd_args(current_args)
--- @type vim.api.keyset.cmd
local args = {
range = current_args.range == 1 and { current_args.line1 }
or current_args.range == 2 and { current_args.line1, current_args.line2 }
or nil,
count = current_args.count ~= -1 and current_args.count or nil,
reg = current_args.reg ~= '' and current_args.reg or nil,
bang = current_args.bang or nil,
args = #current_args.fargs > 0 and current_args.fargs or nil,
mods = current_args.smods,
}
return args return args
end end
--- Gets the current editor dimensions.
--- Useful for positioning floating windows or calculating layout sizes.
---
--- @return { width: number, height: number } The editor dimensions in columns and lines
---
--- @usage
--- ```lua
--- -- Center a floating window:
--- local dims = utils.get_editor_dimensions()
--- local win_width = 80
--- local win_height = 20
--- local col = math.floor((dims.width - win_width) / 2)
--- local row = math.floor((dims.height - win_height) / 2)
---
--- vim.api.nvim_open_win(bufnr, true, {
--- relative = 'editor',
--- width = win_width,
--- height = win_height,
--- col = col,
--- row = row,
--- })
---
--- -- Check if editor is wide enough for side-by-side layout:
--- local dims = utils.get_editor_dimensions()
--- if dims.width >= 160 then
--- -- Use side-by-side layout
--- else
--- -- Use stacked layout
--- end
--- ```
function M.get_editor_dimensions() return { width = vim.go.columns, height = vim.go.lines } end function M.get_editor_dimensions() return { width = vim.go.columns, height = vim.go.lines } end
return M return M

View File

@ -19,5 +19,6 @@ pkgs.mkShell {
pkgs.lua51Packages.nlua pkgs.lua51Packages.nlua
pkgs.neovim pkgs.neovim
pkgs.stylua pkgs.stylua
pkgs.watchexec
]; ];
} }