1 Commits

Author SHA1 Message Date
12945a4cdf (examples/notify.lua) eliminate dependency on non-existent class
All checks were successful
NeoVim tests / code-quality (push) Successful in 1m17s
2025-10-02 19:53:04 -06:00
8 changed files with 153 additions and 415 deletions

View File

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

View File

@@ -1,8 +1,7 @@
local Buffer = require 'u.buffer'
local Renderer = require('u.renderer').Renderer
local TreeBuilder = require('u.renderer').TreeBuilder
local tracker = require 'u.tracker'
local utils = require 'u.utils'
local Window = require 'my.window'
local TIMEOUT = 4000
local ICONS = {
@@ -14,15 +13,25 @@ local ICONS = {
}
local DEFAULT_ICON = { text = '', group = 'DiagnosticSignOk' }
--- @alias Notification {
local S_EDITOR_DIMENSIONS =
tracker.create_signal(utils.get_editor_dimensions(), 's:editor_dimensions')
vim.api.nvim_create_autocmd('VimResized', {
callback = function()
local new_dim = utils.get_editor_dimensions()
S_EDITOR_DIMENSIONS:set(new_dim)
end,
})
--- @alias u.example.Notification {
--- kind: number;
--- id: number;
--- text: string;
--- timer: uv.uv_timer_t;
--- }
local M = {}
--- @type Window | nil
--- @type { win: integer, buf: integer, renderer: u.Renderer } | nil
local notifs_w
local s_notifications_raw = tracker.create_signal {}
@@ -30,44 +39,49 @@ local s_notifications = s_notifications_raw:debounce(50)
-- Render effect:
tracker.create_effect(function()
--- @type Notification[]
--- @type u.example.Notification[]
local notifs = s_notifications:get()
--- @type { width: integer, height: integer }
local editor_size = S_EDITOR_DIMENSIONS:get()
if #notifs == 0 then
if notifs_w then
notifs_w:close(true)
if vim.api.nvim_win_is_valid(notifs_w.win) then vim.api.nvim_win_close(notifs_w.win, true) end
notifs_w = nil
end
return
end
vim.schedule(function()
local editor_size = utils.get_editor_dimensions()
local avail_width = editor_size.width
local float_width = 40
local float_height = math.min(#notifs, editor_size.height - 3)
local win_config = {
relative = 'editor',
anchor = 'NE',
row = 0,
col = avail_width,
width = float_width,
height = math.min(#notifs, editor_size.height - 3),
height = float_height,
border = 'single',
focusable = false,
zindex = 900,
}
vim.schedule(function()
if not notifs_w or not vim.api.nvim_win_is_valid(notifs_w.win) then
notifs_w = Window.new(Buffer.create(false, true), win_config)
vim.wo[notifs_w.win].cursorline = false
vim.wo[notifs_w.win].list = false
vim.wo[notifs_w.win].listchars = ''
vim.wo[notifs_w.win].number = false
vim.wo[notifs_w.win].relativenumber = false
vim.wo[notifs_w.win].wrap = false
local b = vim.api.nvim_create_buf(false, true)
local w = vim.api.nvim_open_win(b, false, win_config)
vim.wo[w].cursorline = false
vim.wo[w].list = false
vim.wo[w].listchars = ''
vim.wo[w].number = false
vim.wo[w].relativenumber = false
vim.wo[w].wrap = false
notifs_w = { win = w, buf = b, renderer = Renderer.new(b) }
else
notifs_w:set_config(win_config)
vim.api.nvim_win_set_config(notifs_w.win, win_config)
end
notifs_w:render(TreeBuilder.new()
notifs_w.renderer:render(TreeBuilder.new()
:nest(function(tb)
for idx, notif in ipairs(notifs) do
if idx > 1 then tb:put '\n' end
@@ -79,48 +93,81 @@ tracker.create_effect(function()
end)
:tree())
vim.api.nvim_win_call(notifs_w.win, function()
-- scroll to bottom:
vim.cmd.normal 'G'
-- scroll all the way to the left:
vim.cmd.normal '9999zh'
vim.fn.winrestview {
-- scroll all the way left:
leftcol = 0,
-- set the bottom line to be at the bottom of the window:
topline = vim.api.nvim_buf_line_count(notifs_w.buf) - win_config.height + 1,
}
end)
end)
end)
local _orig_notify
--- @param msg string
--- @param level integer|nil
--- @param opts table|nil
local function my_notify(msg, level, opts)
vim.schedule(function() _orig_notify(msg, level, opts) end)
if level == nil then level = vim.log.levels.INFO end
if level < vim.log.levels.INFO then return end
local id = math.random(math.huge)
--- @param notifs Notification[]
s_notifications_raw:schedule_update(function(notifs)
table.insert(notifs, { kind = level, id = id, text = msg })
return notifs
end)
vim.defer_fn(function()
--- @param notifs Notification[]
--- @param id number
local function _delete_notif(id)
--- @param notifs u.example.Notification[]
s_notifications_raw:schedule_update(function(notifs)
for i, notif in ipairs(notifs) do
if notif.id == id then
notif.timer:stop()
notif.timer:close()
table.remove(notifs, i)
break
end
end
return notifs
end)
end, TIMEOUT)
end
local _orig_notify
--- @param msg string
--- @param level integer|nil
--- @param opts? { id: number }
function M.notify(msg, level, opts)
if level == nil then level = vim.log.levels.INFO end
opts = opts or {}
local id = opts.id or math.random(999999999)
--- @type u.example.Notification?
local notif = vim.iter(s_notifications_raw:get()):find(function(n) return n.id == id end)
if not notif then
-- Create a new notification (maybe):
if vim.trim(msg) == '' then return id end
if level < vim.log.levels.INFO then return id end
local timer = assert((vim.uv or vim.loop).new_timer(), 'could not create timer')
timer:start(TIMEOUT, 0, function() _delete_notif(id) end)
notif = {
id = id,
kind = level,
text = msg,
timer = timer,
}
--- @param notifs u.example.Notification[]
s_notifications_raw:schedule_update(function(notifs)
table.insert(notifs, notif)
return notifs
end)
else
-- Update an existing notification:
s_notifications_raw:schedule_update(function(notifs)
-- We already have a copy-by-reference of the notif we want to modify:
notif.timer:stop()
notif.text = msg
notif.kind = level
notif.timer:start(TIMEOUT, 0, function() _delete_notif(id) end)
return notifs
end)
end
return id
end
local _once_msgs = {}
local function my_notify_once(msg, level, opts)
function M.notify_once(msg, level, opts)
if vim.tbl_contains(_once_msgs, msg) then return false end
table.insert(_once_msgs, msg)
vim.notify(msg, level, opts)
@@ -130,8 +177,8 @@ end
function M.setup()
if _orig_notify == nil then _orig_notify = vim.notify end
vim.notify = my_notify
vim.notify_once = my_notify_once
vim.notify = M.notify
vim.notify_once = M.notify_once
end
return M

View File

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

View File

@@ -1,13 +1,6 @@
local Pos = require 'u.pos'
local ESC = vim.api.nvim_replace_termcodes('<Esc>', true, false, true)
local NS = vim.api.nvim_create_namespace 'u.range'
---@class u.ExtmarkRange
---@field bufnr number
---@field id number
local ExtmarkRange = {}
ExtmarkRange.__index = ExtmarkRange
--- @class u.Range
--- @field start u.Pos
@@ -90,28 +83,6 @@ function Range.from_marks(lpos, rpos)
return Range.new(start, stop, mode)
end
--- @param bufnr number
--- @param extmark vim.api.keyset.get_extmark_item_by_id
function Range.from_extmark(bufnr, extmark)
---@type integer, integer, vim.api.keyset.extmark_details | nil
local start_row0, start_col0, details = unpack(extmark)
local mode = 'v'
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)
-- Check for invalid extmark range:
if stop and stop < start then return Range.new(stop) end
if stop and stop.col == 0 then
mode = 'V'
stop = stop:must_next(-1)
stop.col = Pos.MAX_COL
end
return Range.new(start, stop, mode)
end
--- @param bufnr? number
function Range.from_buf_text(bufnr)
if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
@@ -301,13 +272,6 @@ function Range:to_linewise()
return r
end
function Range:to_charwise()
local r = self:clone()
r.mode = 'v'
if r.stop:is_col_max() then r.stop = r.stop:as_real() end
return r
end
--- @param x u.Pos | u.Range
function Range:contains(x)
if getmetatable(x) == Pos then
@@ -370,21 +334,6 @@ function Range:save_to_marks(left, right)
end
end
function Range:save_to_extmark()
local r = self:to_charwise()
local end_row = r.stop.lnum - 1
local end_col = r.stop.col
if self.mode == 'V' then
end_row = end_row + 1
end_col = 0
end
local id = vim.api.nvim_buf_set_extmark(r.start.bufnr, NS, r.start.lnum - 1, r.start.col - 1, {
end_row = end_row,
end_col = end_col,
})
return ExtmarkRange.new(r.start.bufnr, id)
end
function Range:set_visual_selection()
if self:is_empty() then return end
if vim.api.nvim_get_current_buf() ~= self.start.bufnr then
@@ -647,17 +596,4 @@ function Range:highlight(group, opts)
}
end
function ExtmarkRange.new(bufnr, id) return setmetatable({ bufnr = bufnr, id = id }, ExtmarkRange) 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
return Range

View File

@@ -1,23 +1,12 @@
local Signal = require('u.tracker').Signal
local M = {}
local H = {}
--- @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.Tag { kind: 'tag'; name: string, attributes: table<string, unknown>, children: u.renderer.Tree }
--- @alias u.renderer.Node nil | boolean | string | u.renderer.Tag
--- @alias u.renderer.Tree u.renderer.Node | u.renderer.Node[]
-- luacheck: ignore
--- @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>
--- @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>
M.h = setmetatable({}, {
__call = function(_, name, attributes, children)
return {
@@ -36,19 +25,14 @@ M.h = setmetatable({}, {
})
-- 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
--- @alias RendererExtmark { id?: number; start: [number, number]; stop: [number, number]; opts: any; tag: any }
--- @class u.renderer.Renderer
--- @class u.Renderer
--- @field bufnr number
--- @field ns number
--- @field changedtick number
--- @field old { lines: string[]; extmarks: u.renderer.RendererExtmark[] }
--- @field curr { lines: string[]; extmarks: u.renderer.RendererExtmark[] }
--- @field old { lines: string[]; extmarks: RendererExtmark[] }
--- @field curr { lines: string[]; extmarks: RendererExtmark[] }
local Renderer = {}
Renderer.__index = Renderer
M.Renderer = Renderer
@@ -80,12 +64,6 @@ function Renderer.new(bufnr) -- {{{
old = { lines = {}, extmarks = {} },
curr = { lines = {}, extmarks = {} },
}, Renderer)
vim.api.nvim_create_autocmd({ 'TextChangedI', 'TextChangedP' }, {
buffer = bufnr,
callback = function() self:_on_text_changed() end,
})
return self
end -- }}}
@@ -221,7 +199,7 @@ function Renderer:render(tree) -- {{{
self.changedtick = changedtick
end
--- @type u.renderer.RendererExtmark[]
--- @type RendererExtmark[]
local extmarks = {}
--- @type string[]
@@ -236,29 +214,32 @@ function Renderer:render(tree) -- {{{
tag.attributes.extmark.hl_group = tag.attributes.extmark.hl_group or hl
end
local extmark_opts = tag.attributes.extmark or {}
local extmark = tag.attributes.extmark
-- Set any necessary keymaps:
for _, mode in ipairs { 'i', 'n', 'v', 'x', 'o' } do
for lhs, _ in pairs(tag.attributes[mode .. 'map'] or {}) do
-- Force creating an extmark if there are key handlers. To accurately
-- sense the bounds of the text, we need an extmark:
extmark = extmark or {}
vim.keymap.set(
mode,
'n',
lhs,
function() return self:_expr_map_callback(mode, lhs) end,
function() return self:_expr_map_callback('n', lhs) end,
{ buffer = self.bufnr, expr = true, replace_keycodes = true }
)
end
end
if extmark then
table.insert(extmarks, {
start = start0,
stop = stop0,
opts = extmark_opts,
opts = extmark,
tag = tag,
})
end
end
end, -- }}}
}
@@ -335,10 +316,9 @@ function Renderer:_expr_map_callback(mode, lhs) -- {{{
local tag = pos_info.tag
-- is the tag listening?
--- @type u.renderer.TagEventHandler?
local f = vim.tbl_get(tag.attributes, mode .. 'map', lhs)
if type(f) == 'function' then
local result = f(tag, mode, lhs)
local result = f()
if result == '' then
-- bubble-up to the next tag, but set cancel to true, in case there are
-- no more tags to bubble up to:
@@ -353,56 +333,20 @@ function Renderer:_expr_map_callback(mode, lhs) -- {{{
return cancel and '' or lhs
end -- }}}
function Renderer:_on_text_changed()
--- @type integer, integer
local l, c = unpack(vim.api.nvim_win_get_cursor(0))
l = l - 1 -- make it actually 0-based
local pos_infos = self:get_pos_infos { l, c }
for _, pos_info in ipairs(pos_infos) do
local extmark_inf = pos_info.extmark
local tag = pos_info.tag
if tag.attributes.signal and getmetatable(tag.attributes.signal) == Signal then
--- @type u.Signal
local signal = tag.attributes.signal
local extmark =
vim.api.nvim_buf_get_extmark_by_id(self.bufnr, self.ns, extmark_inf.id, { details = true })
--- @type integer, integer, vim.api.keyset.extmark_details
local start_row0, start_col0, details = unpack(extmark)
local end_row0, end_col0 = details.end_row, details.end_col
if start_row0 == end_row0 and start_col0 == end_col0 then
-- Invalid extmark
else
local lines = vim.fn.getregion(
{ self.bufnr, start_row0 + 1, start_col0 + 1 },
{ self.bufnr, end_row0 + 1, end_col0 },
{ type = 'v' }
)
local text = table.concat(lines, '\n')
if text ~= signal:get() then
signal:schedule_set(text)
end
end
end
end
end
--- Returns pairs of extmarks and tags associate with said extmarks. The
--- returned tags/extmarks are sorted smallest (innermost) to largest
--- (outermost).
---
--- @private (private for now)
--- @param pos0 [number; number]
--- @return { extmark: u.renderer.RendererExtmark; tag: u.renderer.Tag; }[]
--- @return { extmark: RendererExtmark; tag: u.renderer.Tag; }[]
function Renderer:get_pos_infos(pos0) -- {{{
local cursor_line0, cursor_col0 = pos0[1], pos0[2]
-- 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,
-- because the NeoVim API is over-inclusive in what it returns:
--- @type u.renderer.RendererExtmark[]
--- @type RendererExtmark[]
local intersecting_extmarks = vim
.iter(
vim.api.nvim_buf_get_extmarks(
@@ -413,7 +357,7 @@ function Renderer:get_pos_infos(pos0) -- {{{
{ details = true, overlap = true }
)
)
--- @return u.renderer.RendererExtmark
--- @return RendererExtmark
:map(function(ext)
--- @type number, number, number, { end_row?: number; end_col?: number }|nil
local id, line0, col0, details = unpack(ext)
@@ -424,7 +368,7 @@ function Renderer:get_pos_infos(pos0) -- {{{
end
return { id = id, start = start, stop = stop, opts = details }
end)
--- @param ext u.renderer.RendererExtmark
--- @param ext RendererExtmark
:filter(function(ext)
if ext.stop[1] ~= nil and ext.stop[2] ~= nil then
return cursor_line0 >= ext.start[1]
@@ -440,8 +384,8 @@ function Renderer:get_pos_infos(pos0) -- {{{
-- Sort the tags into smallest (inner) to largest (outer):
table.sort(
intersecting_extmarks,
--- @param x1 u.renderer.RendererExtmark
--- @param x2 u.renderer.RendererExtmark
--- @param x1 RendererExtmark
--- @param x2 RendererExtmark
function(x1, x2)
if
x1.start[1] == x2.start[1]
@@ -463,10 +407,10 @@ function Renderer:get_pos_infos(pos0) -- {{{
-- 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
-- of tags that we need to fire events for:
--- @type { extmark: u.renderer.RendererExtmark; tag: u.renderer.Tag }[]
--- @type { extmark: RendererExtmark; tag: u.renderer.Tag }[]
local matching_tags = vim
.iter(intersecting_extmarks)
--- @param ext u.renderer.RendererExtmark
--- @param ext RendererExtmark
:map(function(ext)
for _, extmark_cache in ipairs(self.curr.extmarks) do
if extmark_cache.id == ext.id then return { extmark = ext, tag = extmark_cache.tag } end
@@ -479,7 +423,7 @@ end -- }}}
-- }}}
-- TreeBuilder {{{
--- @class u.renderer.TreeBuilder
--- @class u.TreeBuilder
--- @field private nodes u.renderer.Node[]
local TreeBuilder = {}
TreeBuilder.__index = TreeBuilder
@@ -491,7 +435,7 @@ function TreeBuilder.new()
end
--- @param nodes u.renderer.Tree
--- @return u.renderer.TreeBuilder
--- @return u.TreeBuilder
function TreeBuilder:put(nodes)
table.insert(self.nodes, nodes)
return self
@@ -500,7 +444,7 @@ end
--- @param name string
--- @param attributes? table<string, any>
--- @param children? u.renderer.Node | u.renderer.Node[]
--- @return u.renderer.TreeBuilder
--- @return u.TreeBuilder
function TreeBuilder:put_h(name, attributes, children)
local tag = M.h(name, attributes, children)
table.insert(self.nodes, tag)
@@ -508,7 +452,7 @@ function TreeBuilder:put_h(name, attributes, children)
end
--- @param fn fun(TreeBuilder): any
--- @return u.renderer.TreeBuilder
--- @return u.TreeBuilder
function TreeBuilder:nest(fn)
local nested_writer = TreeBuilder.new()
fn(nested_writer)

View File

@@ -4,63 +4,15 @@ local M = {}
-- Types
--
--- @class u.utils.QfItem
--- @field col number
--- @field filename string
--- @field kind string
--- @field lnum number
--- @field text string
--- @alias QfItem { col: number, filename: string, kind: string, lnum: number, text: string }
--- @alias KeyMaps table<string, fun(): any | string> }
-- luacheck: ignore
--- @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 }
--- @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
--- @param x `T` The value to debug print
--- @param message? string Optional message to print alongside the value
--- @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")
--- ```
--- @param x `T`
--- @param message? string
--- @return T
function M.dbg(x, message)
local t = {}
if message ~= nil then table.insert(t, message) end
@@ -69,37 +21,22 @@ function M.dbg(x, message)
return x
end
--- Creates a user command with enhanced argument processing.
--- Automatically computes range information and attaches it as `args.info`.
--- A utility for creating user commands that also pre-computes useful information
--- and attaches it to the arguments.
---
--- @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
--- -- Create a command that works with visual selections:
--- utils.ucmd('MyCmd', function(args)
--- -- Print the visually selected text:
--- -- Example:
--- ucmd('MyCmd', function(args)
--- -- print the visually selected text:
--- vim.print(args.info:text())
--- -- Or get the selection as an array of lines:
--- -- or get the vtext as an array of lines:
--- vim.print(args.info:lines())
--- 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
--- @param opts? { nargs?: 0|1|'*'|'?'|'+'; range?: boolean|'%'|number; count?: boolean|number, addr?: string; completion?: string }
function M.ucmd(name, cmd, opts)
local Range = require 'u.range'
@@ -111,81 +48,9 @@ function M.ucmd(name, cmd, opts)
return cmd(args)
end
end
vim.api.nvim_create_user_command(name, cmd2, opts or {} --[[@as any]])
vim.api.nvim_create_user_command(name, cmd2, opts or {})
end
--- Creates command arguments for delegating from one command to another.
--- Preserves all relevant context (range, modifiers, bang, etc.) when
--- implementing a derived command in terms of a base command.
---
--- @param current_args vim.api.keyset.create_user_command.command_args|u.utils.RawCmdArgs The arguments from the current command
--- @return vim.api.keyset.cmd Arguments suitable for vim.cmd() calls
---
--- @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
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
return M

View File

@@ -3,8 +3,8 @@
import
# nixpkgs-unstable (neovim@0.11.2):
(fetchTarball {
url = "https://github.com/nixos/nixpkgs/archive/f72be405a10668b8b00937b452f2145244103ebc.tar.gz";
sha256 = "0m1vnvngpxrawsgg306c9sdhbzsiigjgb03yfbdpa2fsb1fs0zm9";
url = "https://github.com/nixos/nixpkgs/archive/e4b09e47ace7d87de083786b404bf232eb6c89d8.tar.gz";
sha256 = "1a2qvp2yz8j1jcggl1yvqmdxicbdqq58nv7hihmw3bzg9cjyqm26";
})
{ },
}:
@@ -19,6 +19,5 @@ pkgs.mkShell {
pkgs.lua51Packages.nlua
pkgs.neovim
pkgs.stylua
pkgs.watchexec
];
}

View File

@@ -617,54 +617,4 @@ describe('Range', function()
}, vim.api.nvim_buf_get_lines(b, 0, -1, false))
end)
end)
it('can save to extmark', function()
withbuf({
'The quick brown',
'fox',
'jumps',
'over',
'the lazy dog',
}, function()
-- Construct a range over 'fox jumps'
local r = Range.new(Pos.new(nil, 2, 1), Pos.new(nil, 3, 5), 'v')
local extrange = r:save_to_extmark()
assert.are.same({ 'fox', 'jumps' }, extrange:range():lines())
-- change 'jumps' to 'leaps':
vim.api.nvim_buf_set_text(extrange.bufnr, 2, 0, 2, 4, { 'leap' })
assert.are.same({
'The quick brown',
'fox',
'leaps',
'over',
'the lazy dog',
}, vim.api.nvim_buf_get_lines(extrange.bufnr, 0, -1, false))
assert.are.same({ 'fox', 'leaps' }, extrange:range():lines())
end)
end)
it('can save linewise extmark', function()
withbuf({
'The quick brown',
'fox',
'jumps',
'over',
'the lazy dog',
}, function()
-- Construct a range over 'fox jumps'
local r = Range.new(Pos.new(nil, 2, 1), Pos.new(nil, 3, Pos.MAX_COL), 'V')
local extrange = r:save_to_extmark()
assert.are.same({ 'fox', 'jumps' }, extrange:range():lines())
local extmark = vim.api.nvim_buf_get_extmark_by_id(
extrange.bufnr,
vim.api.nvim_create_namespace 'u.range',
extrange.id,
{ details = true }
)
local row0, col0, details = unpack(extmark)
assert.are.same({ 1, 0 }, { row0, col0 })
assert.are.same({ 3, 0 }, { details.end_row, details.end_col })
end)
end)
end)