3 Commits

Author SHA1 Message Date
9199a9bc3a convert to mise
Some checks failed
ci / ci (push) Failing after 3m39s
2026-02-07 19:10:26 -07:00
72b6886838 range: extmarks/tsquery; renderer: text-change
All checks were successful
NeoVim tests / code-quality (push) Successful in 1m18s
2025-10-02 20:01:40 -06:00
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
27 changed files with 401 additions and 165 deletions

22
.emmyrc.json Normal file
View File

@@ -0,0 +1,22 @@
{
"$schema": "https://raw.githubusercontent.com/EmmyLuaLs/emmylua-analyzer-rust/refs/heads/main/crates/emmylua_code_analysis/resources/schema.json",
"diagnostics": {
"disable": [
"access-invisible",
"redefined-local"
]
},
"runtime": {
"version": "LuaJIT"
},
"workspace": {
"ignoreDir": [
".prefix"
],
"library": [
"$VIMRUNTIME",
"library/busted",
"library/luv"
]
}
}

View File

@@ -1,29 +1,60 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: NeoVim tests
on: [push]
name: ci
on:
workflow_dispatch:
pull_request:
push:
tags: ["*"]
branches: ["*"]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
MISE_EXPERIMENTAL: true
jobs:
code-quality:
ci:
runs-on: ubuntu-latest
env:
XDG_CONFIG_HOME: ${{ github.workspace }}/.config/
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v31
with:
nix_path: nixpkgs=channel:nixos-unstable
submodules: true
- name: Populate Nix store
run:
nix-shell --pure --run 'true'
- name: Install apt dependencies
run: |
sudo apt-get update
sudo apt-get install -y libreadline-dev
- name: Type-check with lua-language-server
run:
nix-shell --pure --run 'make lint'
- name: Setup environment
run: |
if [ -n "${{secrets.TOKEN}}" ]; then
export GITHUB_TOKEN="${{secrets.TOKEN}}"
fi
export MISE_GITHUB_TOKEN="$GITHUB_TOKEN"
echo "$GITHUB_TOKEN" >> $GITHUB_ENV
echo "$MISE_GITHUB_TOKEN" >> $GITHUB_ENV
- name: Check formatting with stylua
run:
nix-shell --pure --run 'make fmt-check'
- name: Install mise
run: |
curl https://mise.run | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH
echo "$HOME/.local/share/mise/bin" >> $GITHUB_PATH
echo "$HOME/.local/share/mise/shims" >> $GITHUB_PATH
- name: Run busted tests
run:
nix-shell --pure --run 'make test'
- name: Install mise dependencies
run: |
mise install
mise list --local
- name: Check Lua formatting
run: mise run fmt:check
- name: Check for type-errors
run: mise run lint
- name: Run tests
run: mise run test:all

1
.gitignore vendored
View File

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

12
.gitmodules vendored Normal file
View File

@@ -0,0 +1,12 @@
[submodule "library/busted"]
path = library/busted
url = https://github.com/LuaCATS/busted
branch = main
[submodule "library/luv"]
path = library/luv
url = https://github.com/LuaCATS/luv
branch = main
[submodule "nvimv"]
path = nvimv
url = https://github.com/jrop/nvimv
branch = main

16
.luarc.json Normal file
View File

@@ -0,0 +1,16 @@
{
"$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/refs/heads/master/setting/schema.json",
"runtime": {
"version": "LuaJIT"
},
"workspace": {
"ignoreDir": [
".prefix"
],
"library": [
"$VIMRUNTIME",
"library/busted",
"library/luv"
]
}
}

1
.styluaignore Normal file
View File

@@ -0,0 +1 @@
library/

View File

@@ -1,24 +0,0 @@
all: lint fmt-check test
lint:
@echo "## Typechecking"
@lua-language-server --check=lua/u/ --checklevel=Error
fmt-check:
@echo "## Checking code format"
@stylua --check .
fmt:
@echo "## Formatting code"
@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
watch:
@watchexec -c -e lua make

View File

@@ -10,8 +10,12 @@
-- change on the underlying filesystem.
--------------------------------------------------------------------------------
--- @alias u.examples.FsDir { kind: 'dir'; path: string; expanded: boolean; children: u.examples.FsNode[] }
--- @alias u.examples.FsFile { kind: 'file'; path: string }
--- @class u.examples.FsDir
--- @field kind 'dir'
--- @field path string
--- @field expanded boolean
--- @field children u.examples.FsNode[]
--- @alias u.examples.FsFile table
--- @alias u.examples.FsNode u.examples.FsDir | u.examples.FsFile
--- @alias u.examples.ShowOpts { root_path?: string, width?: number, focus_path?: string }
@@ -58,7 +62,7 @@ function H.relative(path, base)
end
--- @param root_path string
--- @return { tree: u.examples.FsDir; path_to_node: table<string, u.examples.FsNode> }
--- @return { tree: u.examples.FsDir, path_to_node: table<string, u.examples.FsNode> }
function H.get_tree_inf(root_path)
logger:info { 'get_tree_inf', root_path }
--- @type table<string, u.examples.FsNode>
@@ -112,12 +116,11 @@ function H.populate_dir_children(tree, path_to_node)
end)
end
--- @param opts {
--- bufnr: number;
--- prev_winnr: number;
--- root_path: string;
--- focus_path?: string;
--- }
--- @class u.examples.RenderOpts
--- @field bufnr number
--- @field prev_winnr number
--- @field root_path string
--- @field focus_path? string
---
--- @return { expand: fun(path: string), collapse: fun(path: string) }
local function _render_in_buffer(opts)
@@ -310,7 +313,7 @@ local function _render_in_buffer(opts)
--
local renderer = Renderer.new(opts.bufnr)
tracker.create_effect(function()
--- @type { tree: u.examples.FsDir; path_to_node: table<string, u.examples.FsNode> }
--- @type { tree: u.examples.FsDir, path_to_node: table<string, u.examples.FsNode> }
local tree_inf = s_tree_inf:get()
local tree = tree_inf.tree
@@ -406,11 +409,10 @@ end
-- Public API functions:
--------------------------------------------------------------------------------
--- @type {
--- bufnr: number;
--- winnr: number;
--- controller: { expand: fun(path: string), collapse: fun(path: string) };
--- } | nil
--- @class u.examples.CurrentInf
--- @field bufnr number
--- @field winnr number
--- @field controller table
local current_inf = nil
--- Show the filetree:

View File

@@ -1,6 +1,5 @@
local Buffer = require 'u.buffer'
local Renderer = require('u.renderer').Renderer
local TreeBuilder = require('u.renderer').TreeBuilder
local Window = require 'my.window'
local tracker = require 'u.tracker'
local utils = require 'u.utils'
@@ -14,15 +13,24 @@ local ICONS = {
}
local DEFAULT_ICON = { text = '', group = 'DiagnosticSignOk' }
--- @alias u.examples.Notification {
--- kind: number;
--- id: number;
--- text: string;
--- }
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,
})
--- @class u.examples.Notification
--- @field kind number
--- @field id number
--- @field text string
--- @field timer uv.uv_timer_t
local M = {}
--- @type Window | nil
--- @type { win: integer, buf: integer, renderer: u.renderer.Renderer } | nil
local notifs_w
local s_notifications_raw = tracker.create_signal {}
@@ -32,42 +40,47 @@ local s_notifications = s_notifications_raw:debounce(50)
tracker.create_effect(function()
--- @type u.examples.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
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()
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
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 +92,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)
--- @param id number
local function _delete_notif(id)
--- @param notifs u.examples.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 table|nil
local function my_notify(msg, level, opts)
vim.schedule(function() _orig_notify(msg, level, opts) end)
--- @param opts? { id: number }
function M.notify(msg, level, opts)
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)
opts = opts or {}
local id = opts.id or math.random(999999999)
--- @param notifs u.examples.Notification[]
s_notifications_raw:schedule_update(function(notifs)
table.insert(notifs, { kind = level, id = id, text = msg })
return notifs
end)
--- @type u.examples.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
vim.defer_fn(function()
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.examples.Notification[]
s_notifications_raw:schedule_update(function(notifs)
for i, notif in ipairs(notifs) do
if notif.id == id then
table.remove(notifs, i)
break
end
end
table.insert(notifs, notif)
return notifs
end)
end, TIMEOUT)
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 +176,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

@@ -44,23 +44,21 @@ local function shallow_copy_arr(arr) return vim.iter(arr):totable() end
-- shortest portion of this function.
--------------------------------------------------------------------------------
--- @alias u.examples.SelectController {
--- get_items: fun(): T[];
--- set_items: fun(items: T[]);
--- set_filter_text: fun(filter_text: string);
--- get_selected_indices: fun(): number[];
--- get_selected_items: fun(): T[];
--- set_selected_indices: fun(indicies: number[], ephemeral?: boolean);
--- close: fun();
--- }
--- @alias u.examples.SelectOpts<T> {
--- items: `T`[];
--- multi?: boolean;
--- format_item?: fun(item: T): u.renderer.Tree;
--- on_finish?: fun(items: T[], indicies: number[]);
--- on_selection_changed?: fun(items: T[], indicies: number[]);
--- mappings?: table<string, fun(select: u.examples.SelectController)>;
--- }
--- @class u.examples.SelectController<T>
--- @field get_items fun(): T[]
--- @field set_items fun(items: T[])
--- @field set_filter_text fun(filter_text: string)
--- @field get_selected_indices fun(): number[]
--- @field get_selected_items fun(): T[]
--- @field set_selected_indices fun(indices: number[], ephemeral?: boolean)
--- @field close fun()
--- @class u.examples.SelectOpts<T>
--- @field items T[]
--- @field multi? boolean
--- @field format_item? fun(item: T): u.renderer.Tree
--- @field on_finish? fun(items: T[], indices: number[])
--- @field on_selection_changed? fun(items: T[], indices: number[])
--- @field mappings? table<string, fun(select: u.examples.SelectController)>
--- @generic T
--- @param opts u.examples.SelectOpts<T>
@@ -326,7 +324,7 @@ function M.create_picker(opts) -- {{{
safe_wrap(function()
local items = s_items:get()
local selected_indices = s_selected_indices:get()
--- @type { orig_idx: number; item: T }[]
--- @type { orig_idx: number, item: T }[]
local filtered_items = s_filtered_items:get()
local cursor_index = s_cursor_index:get()
local indices = shallow_copy_arr(selected_indices)
@@ -477,7 +475,7 @@ function M.create_picker(opts) -- {{{
local selected_indices = s_selected_indices:get()
local top_offset = s_top_offset:get()
local cursor_index = s_cursor_index:get()
--- @type { filtered_idx: number; orig_idx: number; item: T; formatted: string }[]
--- @type { filtered_idx: number, orig_idx: number, item: T, formatted: string }[]
local visible_items = s_visible_items:get()
-- The above has to run in the execution context for the signaling to work, but
@@ -791,14 +789,14 @@ function M.buffers() -- {{{
-- ensure that `cwd` ends with a trailing slash:
if cwd[#cwd] ~= '/' then cwd = cwd .. '/' end
--- @type { name: string; changed: number; bufnr: number }[]
--- @type { name: string, changed: number, bufnr: number }[]
local bufs = vim.fn.getbufinfo { buflisted = 1 }
M.create_picker {
multi = true,
items = bufs,
--- @param item { name: string; changed: number; bufnr: number }
--- @param item { name: string, changed: number, bufnr: number }
format_item = function(item)
local item_name = item.name
if item_name == '' then item_name = '[No Name]' end

View File

@@ -21,10 +21,10 @@ local surrounds = {
['`'] = { left = '`', right = '`' },
}
--- @type { left: string; right: string } | nil
--- @type { left: string, right: string } | nil
local CACHED_BOUNDS = nil
--- @return { left: string; right: string }|nil
--- @return { left: string, right: string }|nil
local function prompt_for_bounds()
if vim_repeat.is_repeating() then
-- If we are repeating, we don't want to prompt for bounds, because
@@ -55,7 +55,7 @@ local function prompt_for_bounds()
end
--- @param range u.Range
--- @param bounds { left: string; right: string }
--- @param bounds { left: string, right: string }
local function do_surround(range, bounds)
local left = bounds.left
local right = bounds.right

1
library/busted Submodule

Submodule library/busted added at 5ed85d0e01

1
library/luv Submodule

Submodule library/luv added at 3615eb12c9

View File

@@ -62,7 +62,7 @@ end
function Buffer:lines(start, stop) return Range.from_lines(self.bufnr, start, stop) end
--- @param motion string
--- @param opts? { contains_cursor?: boolean; pos?: u.Pos }
--- @param opts? { contains_cursor?: boolean, pos?: u.Pos }
function Buffer:motion(motion, opts)
opts = vim.tbl_extend('force', opts or {}, { bufnr = self.bufnr })
return Range.from_motion(motion, opts)

View File

@@ -3,9 +3,7 @@ local Range = require 'u.range'
--- @type fun(range: u.Range): nil|(fun():any)
local __U__OpKeymapOpFunc_rhs = nil
--- This is the global utility function used for operatorfunc
--- in opkeymap
--- @type nil|fun(range: u.Range): fun():any|nil
--- This is the global utility function used for operatorfunc in opkeymap
--- @param ty 'line'|'char'|'block'
-- selene: allow(unused_variable)
function _G.__U__OpKeymapOpFunc(ty)

View File

@@ -112,7 +112,7 @@ function Range.from_lines(bufnr, start_line, stop_line)
end
--- @param motion string
--- @param opts? { bufnr?: number; contains_cursor?: boolean; pos?: u.Pos, user_defined?: boolean }
--- @param opts? { bufnr?: number, contains_cursor?: boolean, pos?: u.Pos, user_defined?: boolean }
--- @return u.Range|nil
function Range.from_motion(motion, opts)
-- Options handling:
@@ -513,7 +513,7 @@ function Range:text() return vim.fn.join(self:lines(), '\n') end
--- @param l number
-- luacheck: ignore
--- @return { line: string; idx0: { start: number; stop: number; }; lnum: number; range: fun():u.Range; text: fun():string }|nil
--- @return { line: string, idx0: { start: number, stop: number }, lnum: number, range: fun():u.Range, text: fun():string }|nil
function Range:line(l)
if l < 0 then l = self:line_count() + l + 1 end
if l > self:line_count() then return end

View File

@@ -15,7 +15,7 @@ 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>, on_change?: fun(text: string): unknown }
--- @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>, on_change?: fun(text: string): unknown }
--- @class u.renderer.Tag
--- @field kind 'tag'
@@ -56,8 +56,8 @@ M.h = setmetatable({}, {
--- @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: u.renderer.RendererExtmark[] }
--- @field curr { lines: string[], extmarks: u.renderer.RendererExtmark[] }
local Renderer = {}
Renderer.__index = Renderer
M.Renderer = Renderer
@@ -98,10 +98,9 @@ function Renderer.new(bufnr) -- {{{
return self
end -- }}}
--- @param opts {
--- tree: u.renderer.Tree;
--- on_tag?: fun(tag: u.renderer.Tag, start0: [number, number], stop0: [number, number]): any;
--- }
--- @class u.renderer.MarkupOpts
--- @field tree u.renderer.Tree
--- @field on_tag? fun(tag: u.renderer.Tag, start0: [number, number], stop0: [number, number]): any
function Renderer.markup_to_lines(opts) -- {{{
--- @type string[]
local lines = {}
@@ -158,10 +157,9 @@ function Renderer.markup_to_lines(opts) -- {{{
return lines
end -- }}}
--- @param opts {
--- tree: u.renderer.Tree;
--- format_tag?: fun(tag: u.renderer.Tag): string;
--- }
--- @class u.renderer.StringOpts
--- @field tree u.renderer.Tree
--- @field format_tag? fun(tag: u.renderer.Tag): string
function Renderer.markup_to_string(opts) return table.concat(Renderer.markup_to_lines(opts), '\n') end
--- @param bufnr number
@@ -534,9 +532,9 @@ end -- }}}
--- (outermost).
---
--- @private (private for now)
--- @param pos0 [number; number]
--- @param pos0 [number, number]
--- @param mode string?
--- @return { extmark: u.renderer.RendererExtmark; tag: u.renderer.Tag; }[]
--- @return { extmark: u.renderer.RendererExtmark, tag: u.renderer.Tag }[]
function Renderer:get_tags_at(pos0, mode) -- {{{
local cursor_line0, cursor_col0 = pos0[1], pos0[2]
if not mode then mode = vim.api.nvim_get_mode().mode end
@@ -560,9 +558,8 @@ function Renderer:get_tags_at(pos0, mode) -- {{{
--- @type u.renderer.RendererExtmark[]
local mapped_extmarks = vim
.iter(raw_overlapping_extmarks)
--- @return u.renderer.RendererExtmark
: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 start = { line0, col0 }
local stop = { line0, col0 }
@@ -575,7 +572,6 @@ function Renderer:get_tags_at(pos0, mode) -- {{{
local intersecting_extmarks = vim
.iter(mapped_extmarks)
--- @param ext u.renderer.RendererExtmark
:filter(function(ext)
if ext.stop[1] ~= nil and ext.stop[2] ~= nil then
-- If we've "ciw" and "collapsed" an extmark onto the cursor,
@@ -640,10 +636,9 @@ function Renderer:get_tags_at(pos0, mode) -- {{{
-- 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: u.renderer.RendererExtmark, tag: u.renderer.Tag }[]
local matching_tags = vim
.iter(intersecting_extmarks)
--- @param ext u.renderer.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
@@ -656,7 +651,7 @@ end -- }}}
--- @private
--- @param tag_or_id string | u.renderer.Tag
--- @return { start: [number, number]; stop: [number, number] } | nil
--- @return { start: [number, number], stop: [number, number] } | nil
function Renderer:get_tag_bounds(tag_or_id) -- {{{
for _, x in ipairs(self.curr.extmarks) do
local pos = { start = x.start, stop = x.stop }
@@ -733,12 +728,12 @@ function TreeBuilder:tree() return self.nodes end
-- Levenshtein utility {{{
-- luacheck: ignore
--- @alias u.renderer.LevenshteinChange<T> ({ kind: 'add'; item: T; index: number; } | { kind: 'delete'; item: T; index: number; } | { kind: 'change'; from: T; to: T; index: number; })
--- @alias u.renderer.LevenshteinChange<T> ({ kind: 'add', item: T, index: number } | { kind: 'delete', item: T, index: number } | { kind: 'change', from: T, to: T, index: number })
--- @private
--- @generic T
--- @param x `T`[]
--- @param y T[]
--- @param cost? { of_delete?: fun(x: T): number; of_add?: fun(x: T): number; of_change?: fun(x: T, y: T): number; }
--- @param cost? { of_delete?: (fun(x: T): number), of_add?: (fun(x: T): number), of_change?: (fun(x: T, y: T): number) }
--- @return u.renderer.LevenshteinChange<T>[] The changes, from last (greatest index) to first (smallest index).
function H.levenshtein(x, y, cost)
-- At the moment, this whole `cost` plumbing is not used. Deletes have the

107
mise.toml Normal file
View File

@@ -0,0 +1,107 @@
################################################################################
## Tool Alias
################################################################################
################################################################################
## Tools
################################################################################
[tools]
# nvimv needs jq:
jq = "1.8.1"
"asdf:mise-plugins/mise-lua" = "5.1.5"
# Since we have busted configured to look for "nvim", have a "default" version
# installed. In the tests, we will override with `eval $(nvimv env VERSION)`,
# but to avoid having to litter a bunch of commands with that environment
# initialization, this just makes things simpler:
neovim = "0.11.5"
stylua = "2.3.1"
"cargo:emmylua_ls" = "0.20.0"
"cargo:emmylua_check" = "0.20.0"
################################################################################
# Env
################################################################################
[env]
ASDF_LUA_LUAROCKS_VERSION = "3.12.2"
_.source = { path = "./scripts/env.sh", tools = true }
################################################################################
# Tasks
################################################################################
[tasks]
lint = "emmylua_check --ignore '.prefix/**/*.*' ."
fmt = "stylua ."
"fmt:check" = "stylua --check ."
"test:prepare" = '''
# Install Lua test dependencies (busted, etc.):
luarocks test --prepare
echo
# Check that the nightly version of Neovim is not more than a day old:
if find .prefix -path '**/nightly/**/nvim' -mtime -1 2>/dev/null | grep -q .; then
echo "Neovim Nightly is up-to-date"
else
if ./nvimv/nvimv ls | grep nightly >/dev/null; then
./nvimv/nvimv upgrade nightly
else
./nvimv/nvimv install nightly
fi
fi
echo
'''
[tasks."test:version:no-prep"]
hide = true
usage = '''
arg "<version>" help="The version of Neovim to test with"
'''
run = '''
echo
echo -----------------------------
echo -----------------------------
echo Neovim version=$usage_version
echo -----------------------------
echo -----------------------------
echo
./nvimv/nvimv install $usage_version
eval $(./nvimv/nvimv env $usage_version)
busted --verbose
'''
[tasks."test:version"]
depends = ["test:prepare"]
usage = '''
arg "<version>" help="The version of Neovim to test with"
'''
run = 'mise test:version:no-prep $usage_version'
[tasks."test"]
run = 'mise test:version 0.11.5'
[tasks."test:all"]
depends = ["test:prepare"]
run = '''
VERSIONS="0.11.5 nightly"
for v in $VERSIONS; do
mise test:version:no-prep $v
done
'''
[tasks."test:coverage"]
depends = ["test:prepare"]
run = '''
rm -f ./luacov.*.*
busted --coverage --verbose
luacov
awk '/^Summary$/{flag=1;next} flag{print}' luacov.report.out
'''
[tasks."ci"]
run = '''
mise run fmt:check
mise run lint
mise run test:all
'''

1
nvimv Submodule

Submodule nvimv added at bd5c243b96

6
scripts/env.sh Normal file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
export PREFIX="$(pwd)/.prefix"
export VIMRUNTIME="$(nvim -u NORC --headless +'echo $VIMRUNTIME' +'quitall' 2>&1)"
eval $(luarocks path)

View File

@@ -1,3 +1,4 @@
--- @diagnostic disable: undefined-field, need-check-nil
local Buffer = require 'u.buffer'
local withbuf = loadfile './spec/withbuf.lua'()

View File

@@ -1,3 +1,4 @@
--- @diagnostic disable: undefined-field, need-check-nil, need-check-nil
local CodeWriter = require 'u.codewriter'
describe('CodeWriter', function()

View File

@@ -1,3 +1,4 @@
--- @diagnostic disable: undefined-field, need-check-nil
local Pos = require 'u.pos'
local withbuf = loadfile './spec/withbuf.lua'()

View File

@@ -1,3 +1,4 @@
--- @diagnostic disable: undefined-field, need-check-nil
local Pos = require 'u.pos'
local Range = require 'u.range'
local withbuf = loadfile './spec/withbuf.lua'()

View File

@@ -1,3 +1,4 @@
--- @diagnostic disable: undefined-field, need-check-nil
local R = require 'u.renderer'
local withbuf = loadfile './spec/withbuf.lua'()

View File

@@ -1,3 +1,4 @@
--- @diagnostic disable: undefined-field, need-check-nil
local tracker = require 'u.tracker'
local Signal = tracker.Signal
local ExecutionContext = tracker.ExecutionContext

16
u-0.0.0-0.rockspec Normal file
View File

@@ -0,0 +1,16 @@
rockspec_format = '3.0'
package = 'u'
version = '0.0.0-0'
source = {
url = 'https://github.com/jrop/u.nvim',
}
dependencies = {
'lua = 5.1',
}
test_dependencies = {
'busted == 2.2.0-1',
'luacov == 0.16.0-1',
}