3 Commits

Author SHA1 Message Date
7f85848620 (nix) update commit hash
All checks were successful
NeoVim tests / code-quality (push) Successful in 1m28s
2025-06-11 21:51:09 -06:00
0b72e1c0f9 extmarks: better linewise/charwise handling
All checks were successful
NeoVim tests / code-quality (push) Successful in 1m22s
2025-06-11 21:07:22 -06:00
859187585b range: add extmark utilities
All checks were successful
NeoVim tests / code-quality (push) Successful in 1m22s
2025-06-11 20:04:46 -06:00
5 changed files with 178 additions and 103 deletions

View File

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

View File

@@ -1,6 +1,13 @@
local Pos = require 'u.pos' local Pos = require 'u.pos'
local ESC = vim.api.nvim_replace_termcodes('<Esc>', true, false, true) 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 --- @class u.Range
--- @field start u.Pos --- @field start u.Pos
@@ -83,6 +90,30 @@ function Range.from_marks(lpos, rpos)
return Range.new(start, stop, mode) return Range.new(start, stop, mode)
end end
--- @param bufnr number
--- @param id number
function Range.from_extmark(bufnr, id)
local mode = 'v'
---@type integer, integer, vim.api.keyset.extmark_details | nil
local start_row0, start_col0, details =
unpack(vim.api.nvim_buf_get_extmark_by_id(bufnr, NS, id, { details = true }))
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 --- @param bufnr? number
function Range.from_buf_text(bufnr) function Range.from_buf_text(bufnr)
if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
@@ -272,6 +303,13 @@ function Range:to_linewise()
return r return r
end 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 --- @param x u.Pos | u.Range
function Range:contains(x) function Range:contains(x)
if getmetatable(x) == Pos then if getmetatable(x) == Pos then
@@ -334,6 +372,21 @@ function Range:save_to_marks(left, right)
end end
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() function Range:set_visual_selection()
if self:is_empty() then return end if self:is_empty() then return end
if vim.api.nvim_get_current_buf() ~= self.start.bufnr then if vim.api.nvim_get_current_buf() ~= self.start.bufnr then
@@ -596,4 +649,10 @@ function Range:highlight(group, opts)
} }
end 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:delete() vim.api.nvim_buf_del_extmark(self.bufnr, NS, self.id) end
return Range return Range

View File

@@ -7,6 +7,8 @@ local M = {}
--- @alias QfItem { col: number, filename: string, kind: string, lnum: number, text: string } --- @alias QfItem { col: number, filename: string, kind: string, lnum: number, text: string }
--- @alias KeyMaps table<string, fun(): any | string> } --- @alias KeyMaps table<string, fun(): any | string> }
-- luacheck: ignore -- luacheck: ignore
--- @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 }
-- 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 } --- @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 }
--- @generic T --- @generic T
@@ -51,6 +53,17 @@ function M.ucmd(name, cmd, opts)
vim.api.nvim_create_user_command(name, cmd2, opts or {}) vim.api.nvim_create_user_command(name, cmd2, opts or {})
end end
--- @param current_args RawCmdArgs
function M.create_forward_cmd_args(current_args)
local args = { args = current_args.fargs }
if current_args.range == 1 then
args.range = { current_args.line1 }
elseif current_args.range == 2 then
args.range = { current_args.line1, current_args.line2 }
end
return args
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

@@ -3,8 +3,8 @@
import import
# nixpkgs-unstable (neovim@0.11.2): # nixpkgs-unstable (neovim@0.11.2):
(fetchTarball { (fetchTarball {
url = "https://github.com/nixos/nixpkgs/archive/e4b09e47ace7d87de083786b404bf232eb6c89d8.tar.gz"; url = "https://github.com/nixos/nixpkgs/archive/f72be405a10668b8b00937b452f2145244103ebc.tar.gz";
sha256 = "1a2qvp2yz8j1jcggl1yvqmdxicbdqq58nv7hihmw3bzg9cjyqm26"; sha256 = "0m1vnvngpxrawsgg306c9sdhbzsiigjgb03yfbdpa2fsb1fs0zm9";
}) })
{ }, { },
}: }:

View File

@@ -617,4 +617,54 @@ describe('Range', function()
}, vim.api.nvim_buf_get_lines(b, 0, -1, false)) }, vim.api.nvim_buf_get_lines(b, 0, -1, false))
end) end)
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) end)