1 Commits

Author SHA1 Message Date
4dfadf97e5 update lua API to 1-based indices; add renderer
All checks were successful
NeoVim tests / plenary-tests (push) Successful in 10s
2025-05-04 15:22:19 -06:00
12 changed files with 164 additions and 206 deletions

13
.busted
View File

@@ -1,13 +0,0 @@
return {
_all = {
coverage = false,
lpath = "lua/?.lua;lua/?/init.lua",
lua = "nlua",
},
default = {
verbose = true
},
tests = {
verbose = true
},
}

View File

@@ -2,28 +2,15 @@
name: NeoVim tests
on: [push]
jobs:
code-quality:
plenary-tests:
runs-on: ubuntu-latest
env:
XDG_CONFIG_HOME: ${{ github.workspace }}/.config/
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v31
- uses: rhysd/action-setup-vim@v1
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Populate Nix store
run:
nix-shell --run 'true'
- name: Type-check with lua-language-server
run:
nix-shell --run 'make lint'
- name: Check formatting with stylua
run:
nix-shell --run 'make fmt-check'
- name: Run busted tests
run:
nix-shell --run 'make test'
neovim: true
version: v0.11.0
arch: 'x86_64'
- run: make test

3
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/.lux/
lux.lock
*.src.rock
*.aider*
luacov.*.out

View File

@@ -1,6 +0,0 @@
return {
include = {
'lua/u/',
},
tick = true,
}

View File

@@ -1,21 +1,16 @@
all: lint fmt-check test
PLENARY_DIR=~/.local/share/nvim/site/pack/test/opt/plenary.nvim
all: lint test
lint:
@echo "## Typechecking"
@lua-language-server --check=lua/u/ --checklevel=Error
fmt-check:
@echo "## Checking code format"
@stylua --check .
lua-language-server --check=lua/u/ --checklevel=Error
lx check
fmt:
@echo "## Formatting code"
@stylua .
stylua .
test:
@rm -f luacov.*.out
@echo "## Running tests"
@busted --coverage --verbose
@echo "## Generating coverage report"
@luacov
@awk '/^Summary$$/{flag=1;next} flag{print}' luacov.report.out
test: $(PLENARY_DIR)
NVIM_APPNAME=noplugstest nvim -u NORC --headless -c 'set packpath+=~/.local/share/nvim/site' -c 'packadd plenary.nvim' -c "PlenaryBustedDirectory spec/"
$(PLENARY_DIR):
git clone https://github.com/nvim-lua/plenary.nvim/ $(PLENARY_DIR)

View File

@@ -1,7 +1,8 @@
local Renderer = require('u.renderer').Renderer
local Buffer = require 'u.buffer'
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 = {
@@ -13,25 +14,15 @@ local ICONS = {
}
local DEFAULT_ICON = { text = '', group = 'DiagnosticSignOk' }
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 {
--- @alias Notification {
--- kind: number;
--- id: number;
--- text: string;
--- timer: uv.uv_timer_t;
--- }
local M = {}
--- @type { win: integer, buf: integer, renderer: u.Renderer } | nil
--- @type Window | nil
local notifs_w
local s_notifications_raw = tracker.create_signal {}
@@ -39,49 +30,44 @@ local s_notifications = s_notifications_raw:debounce(50)
-- Render effect:
tracker.create_effect(function()
--- @type u.example.Notification[]
--- @type 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
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
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 = float_height,
height = math.min(#notifs, editor_size.height - 3),
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
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) }
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
else
vim.api.nvim_win_set_config(notifs_w.win, win_config)
notifs_w:set_config(win_config)
end
notifs_w.renderer:render(TreeBuilder.new()
notifs_w:render(TreeBuilder.new()
:nest(function(tb)
for idx, notif in ipairs(notifs) do
if idx > 1 then tb:put '\n' end
@@ -93,81 +79,48 @@ tracker.create_effect(function()
end)
:tree())
vim.api.nvim_win_call(notifs_w.win, function()
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,
}
-- scroll to bottom:
vim.cmd.normal 'G'
-- scroll all the way to the left:
vim.cmd.normal '9999zh'
end)
end)
end)
--- @param id number
local function _delete_notif(id)
--- @param notifs u.example.Notification[]
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[]
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
--- @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, TIMEOUT)
end
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
table.insert(_once_msgs, msg)
vim.notify(msg, level, opts)
@@ -177,8 +130,8 @@ end
function M.setup()
if _orig_notify == nil then _orig_notify = vim.notify end
vim.notify = M.notify
vim.notify_once = M.notify_once
vim.notify = my_notify
vim.notify_once = my_notify_once
end
return M

View File

@@ -1,6 +1,14 @@
local Pos = require 'u.pos'
local ESC = vim.api.nvim_replace_termcodes('<Esc>', true, false, true)
-- Certain functions in the Range class yank text. In order to prevent unwanted
-- highlighting, we intercept and discard some calls to the `on_yank` callback.
local orig_on_yank = vim.hl.on_yank
local on_yank_enabled = true
--- @diagnostic disable-next-line: duplicate-set-field
function vim.hl.on_yank(opts)
if not on_yank_enabled then return end
return orig_on_yank(opts)
end
--- @class u.Range
--- @field start u.Pos
@@ -125,6 +133,10 @@ function Range.from_motion(motion, opts)
local is_txtobj = scope == 'a' or scope == 'i'
local is_quote_txtobj = is_txtobj and vim.tbl_contains({ "'", '"', '`' }, motion_rest)
--- @type u.Pos
local start
--- @type u.Pos
local stop
-- Capture the original state of the buffer for restoration later.
local original_state = {
winview = vim.fn.winsaveview(),
@@ -132,65 +144,59 @@ function Range.from_motion(motion, opts)
cursor = vim.fn.getpos '.',
pos_lbrack = vim.fn.getpos "'[",
pos_rbrack = vim.fn.getpos "']",
opfunc = vim.go.operatorfunc,
prev_captured_range = _G.Range__from_motion_opfunc_captured_range,
prev_mode = vim.fn.mode(),
vinf = Range.from_vtext(),
}
--- @type u.Range|nil
_G.Range__from_motion_opfunc_captured_range = nil
vim.api.nvim_buf_call(opts.bufnr, function()
if opts.pos ~= nil then opts.pos:save_to_pos '.' end
_G.Range__from_motion_opfunc = function(ty)
_G.Range__from_motion_opfunc_captured_range = Range.from_op_func(ty)
end
vim.go.operatorfunc = 'v:lua.Range__from_motion_opfunc'
Pos.invalid():save_to_pos "'["
Pos.invalid():save_to_pos "']"
local prev_on_yank_enabled = on_yank_enabled
on_yank_enabled = false
vim.cmd {
cmd = 'normal',
bang = not opts.user_defined,
args = { ESC .. 'g@' .. motion },
args = { '""y' .. motion },
mods = { silent = true },
}
end)
local captured_range = _G.Range__from_motion_opfunc_captured_range
on_yank_enabled = prev_on_yank_enabled
start = Pos.from_pos "'["
stop = Pos.from_pos "']"
end)
-- Restore original state:
vim.fn.winrestview(original_state.winview)
vim.fn.setreg('"', original_state.regquote)
vim.fn.setpos('.', original_state.cursor)
vim.fn.setpos("'[", original_state.pos_lbrack)
vim.fn.setpos("']", original_state.pos_rbrack)
if original_state.prev_mode ~= 'n' then original_state.vinf:set_visual_selection() end
vim.go.operatorfunc = original_state.opfunc
_G.Range__from_motion_opfunc_captured_range = original_state.prev_captured_range
if not captured_range then return nil end
if start == stop and start:is_invalid() then return nil end
-- Fixup the bounds:
if
-- I have no idea why, but when yanking `i"`, the stop-mark is
-- placed on the ending quote. For other text-objects, the stop-
-- mark is placed before the closing character.
(is_quote_txtobj and scope == 'i' and captured_range.stop:char() == motion_rest)
(is_quote_txtobj and scope == 'i' and stop:char() == motion_rest)
-- *Sigh*, this also sometimes happens for `it` as well.
or (motion == 'it' and captured_range.stop:char() == '<')
or (motion == 'it' and stop:char() == '<')
then
captured_range.stop = captured_range.stop:next(-1) or captured_range.stop
stop = stop:next(-1) or stop
end
if is_quote_txtobj and scope == 'a' then
captured_range.start = captured_range.start:find_next(1, motion_rest) or captured_range.start
captured_range.stop = captured_range.stop:find_next(-1, motion_rest) or captured_range.stop
start = start:find_next(1, motion_rest) or start
stop = stop:find_next(-1, motion_rest) or stop
end
if
opts.contains_cursor and not captured_range:contains(Pos.new(unpack(original_state.cursor)))
opts.contains_cursor
and not Range.new(start, stop):contains(Pos.new(unpack(original_state.cursor)))
then
return nil
end
return captured_range
return Range.new(start, stop)
end
--- Get range information from the currently selected visual text.

19
lux.toml Normal file
View File

@@ -0,0 +1,19 @@
package = "u.nvim"
version = "0.2.0"
lua = ">=5.1"
[description]
summary = ""
maintainer = "jrop"
labels = [ "library", "neovim", "neovim-plugin", "range", "utility" ]
[dependencies]
# Add your dependencies here
# `busted = ">=2.0"`
[run]
args = [ "src/main.lua" ]
[build]
type = "builtin"

4
selene.toml Normal file
View File

@@ -0,0 +1,4 @@
std = "vim"
[lints]
multiple_statements = "allow"

View File

@@ -1,23 +0,0 @@
{
pkgs ?
import
# nixpkgs-unstable (neovim@0.11.2):
(fetchTarball {
url = "https://github.com/nixos/nixpkgs/archive/e4b09e47ace7d87de083786b404bf232eb6c89d8.tar.gz";
sha256 = "1a2qvp2yz8j1jcggl1yvqmdxicbdqq58nv7hihmw3bzg9cjyqm26";
})
{ },
}:
pkgs.mkShell {
packages = [
pkgs.git
pkgs.gnumake
pkgs.lua-language-server
pkgs.lua51Packages.busted
pkgs.lua51Packages.luacov
pkgs.lua51Packages.luarocks
pkgs.lua51Packages.nlua
pkgs.neovim
pkgs.stylua
];
}

View File

@@ -1,4 +1,3 @@
require 'luacov'
local function withbuf(lines, f)
vim.go.swapfile = false

36
vim.yml Normal file
View File

@@ -0,0 +1,36 @@
---
base: lua51
globals:
vim:
any: true
assert.are.same:
args:
- type: any
- type: any
assert.are_not.same:
args:
- type: any
- type: any
assert.has.error:
args:
- type: any
- type: any
assert.is_true:
args:
- type: any
- type: any
assert.is_false:
args:
- type: any
- type: any
describe:
args:
- type: string
- type: function
it:
args:
- type: string
- type: function
before_each:
args:
- type: function