Compare commits
5 Commits
react
...
a4058e11c4
| Author | SHA1 | Date | |
|---|---|---|---|
| a4058e11c4 | |||
| 9054f4453f | |||
| 7f85848620 | |||
| 0b72e1c0f9 | |||
| 859187585b |
2
.busted
2
.busted
@@ -2,7 +2,7 @@ return {
|
|||||||
_all = {
|
_all = {
|
||||||
coverage = false,
|
coverage = false,
|
||||||
lpath = "lua/?.lua;lua/?/init.lua",
|
lpath = "lua/?.lua;lua/?/init.lua",
|
||||||
lua = "nvim -u NONE -i NONE -l",
|
lua = "nlua",
|
||||||
},
|
},
|
||||||
default = {
|
default = {
|
||||||
verbose = true
|
verbose = true
|
||||||
|
|||||||
12
.emmyrc.json
12
.emmyrc.json
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://raw.githubusercontent.com/EmmyLuaLs/emmylua-analyzer-rust/refs/heads/main/crates/emmylua_code_analysis/resources/schema.json",
|
|
||||||
"runtime": {
|
|
||||||
"version": "LuaJIT"
|
|
||||||
},
|
|
||||||
"workspace": {
|
|
||||||
"library": [
|
|
||||||
"$VIMRUNTIME",
|
|
||||||
"library/busted"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
10
.github/workflows/ci.yaml
vendored
10
.github/workflows/ci.yaml
vendored
@@ -14,16 +14,16 @@ jobs:
|
|||||||
|
|
||||||
- name: Populate Nix store
|
- name: Populate Nix store
|
||||||
run:
|
run:
|
||||||
nix-shell --pure --run 'true'
|
nix-shell --run 'true'
|
||||||
|
|
||||||
- name: Type-check
|
- name: Type-check with lua-language-server
|
||||||
run:
|
run:
|
||||||
nix-shell --pure --run 'make lint'
|
nix-shell --run 'make lint'
|
||||||
|
|
||||||
- name: Check formatting with stylua
|
- name: Check formatting with stylua
|
||||||
run:
|
run:
|
||||||
nix-shell --pure --run 'make fmt-check'
|
nix-shell --run 'make fmt-check'
|
||||||
|
|
||||||
- name: Run busted tests
|
- name: Run busted tests
|
||||||
run:
|
run:
|
||||||
nix-shell --pure --run 'make test'
|
nix-shell --run 'make test'
|
||||||
|
|||||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -1,4 +0,0 @@
|
|||||||
[submodule "library/busted"]
|
|
||||||
path = library/busted
|
|
||||||
url = https://github.com/LuaCATS/busted
|
|
||||||
branch = main
|
|
||||||
2
.luacheckrc
Normal file
2
.luacheckrc
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- :vim set ft=lua
|
||||||
|
globals = { "vim" }
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
when:
|
|
||||||
- event: push
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: build
|
|
||||||
image: nixos/nix
|
|
||||||
commands:
|
|
||||||
- nix-shell --pure --run 'make lint'
|
|
||||||
- nix-shell --pure --run 'make fmt-check'
|
|
||||||
- nix-shell --pure --run 'make test'
|
|
||||||
4
Makefile
4
Makefile
@@ -1,10 +1,8 @@
|
|||||||
export VIMRUNTIME := $(shell nvim -u NORC --headless +'echo $$VIMRUNTIME' +'quitall' 2>&1)
|
|
||||||
|
|
||||||
all: lint fmt-check test
|
all: lint fmt-check test
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
@echo "## Typechecking"
|
@echo "## Typechecking"
|
||||||
@emmylua_check .
|
@lua-language-server --check=lua/u/ --checklevel=Error
|
||||||
|
|
||||||
fmt-check:
|
fmt-check:
|
||||||
@echo "## Checking code format"
|
@echo "## Checking code format"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
local Buffer = require 'u.buffer'
|
|
||||||
local tracker = require 'u.tracker'
|
local tracker = require 'u.tracker'
|
||||||
|
local Buffer = require 'u.buffer'
|
||||||
local h = require('u.renderer').h
|
local h = require('u.renderer').h
|
||||||
|
|
||||||
-- Create an buffer for the UI
|
-- Create an buffer for the UI
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
-- change on the underlying filesystem.
|
-- change on the underlying filesystem.
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
--- @alias u.examples.FsDir { kind: 'dir', path: string, expanded: boolean, children: u.examples.FsNode[] }
|
--- @alias FsDir { kind: 'dir'; path: string; expanded: boolean; children: FsNode[] }
|
||||||
--- @alias u.examples.FsFile { kind: 'file', path: string }
|
--- @alias FsFile { kind: 'file'; path: string }
|
||||||
--- @alias u.examples.FsNode u.examples.FsDir | u.examples.FsFile
|
--- @alias FsNode FsDir | FsFile
|
||||||
--- @alias u.examples.ShowOpts { root_path?: string, width?: number, focus_path?: string }
|
--- @alias ShowOpts { root_path?: string, width?: number, focus_path?: string }
|
||||||
|
|
||||||
local Buffer = require 'u.buffer'
|
local Buffer = require 'u.buffer'
|
||||||
local Renderer = require('u.renderer').Renderer
|
local Renderer = require('u.renderer').Renderer
|
||||||
@@ -58,13 +58,13 @@ function H.relative(path, base)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- @param root_path string
|
--- @param root_path string
|
||||||
--- @return { tree: u.examples.FsDir, path_to_node: table<string, u.examples.FsNode> }
|
--- @return { tree: FsDir; path_to_node: table<string, FsNode> }
|
||||||
function H.get_tree_inf(root_path)
|
function H.get_tree_inf(root_path)
|
||||||
logger:info { 'get_tree_inf', root_path }
|
logger:info { 'get_tree_inf', root_path }
|
||||||
--- @type table<string, u.examples.FsNode>
|
--- @type table<string, FsNode>
|
||||||
local path_to_node = {}
|
local path_to_node = {}
|
||||||
|
|
||||||
--- @type u.examples.FsDir
|
--- @type FsDir
|
||||||
local tree = {
|
local tree = {
|
||||||
kind = 'dir',
|
kind = 'dir',
|
||||||
path = H.normalize(root_path or '.'),
|
path = H.normalize(root_path or '.'),
|
||||||
@@ -77,8 +77,8 @@ function H.get_tree_inf(root_path)
|
|||||||
return { tree = tree, path_to_node = path_to_node }
|
return { tree = tree, path_to_node = path_to_node }
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param tree u.examples.FsDir
|
--- @param tree FsDir
|
||||||
--- @param path_to_node table<string, u.examples.FsNode>
|
--- @param path_to_node table<string, FsNode>
|
||||||
function H.populate_dir_children(tree, path_to_node)
|
function H.populate_dir_children(tree, path_to_node)
|
||||||
tree.children = {}
|
tree.children = {}
|
||||||
|
|
||||||
@@ -113,10 +113,10 @@ function H.populate_dir_children(tree, path_to_node)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- @param opts {
|
--- @param opts {
|
||||||
--- bufnr: number,
|
--- bufnr: number;
|
||||||
--- prev_winnr: number,
|
--- prev_winnr: number;
|
||||||
--- root_path: string,
|
--- root_path: string;
|
||||||
--- focus_path?: string
|
--- focus_path?: string;
|
||||||
--- }
|
--- }
|
||||||
---
|
---
|
||||||
--- @return { expand: fun(path: string), collapse: fun(path: string) }
|
--- @return { expand: fun(path: string), collapse: fun(path: string) }
|
||||||
@@ -135,7 +135,7 @@ local function _render_in_buffer(opts)
|
|||||||
local parts = H.split_path(H.relative(focused_path, tree_inf.tree.path))
|
local parts = H.split_path(H.relative(focused_path, tree_inf.tree.path))
|
||||||
local path_to_node = tree_inf.path_to_node
|
local path_to_node = tree_inf.path_to_node
|
||||||
|
|
||||||
--- @param node u.examples.FsDir
|
--- @param node FsDir
|
||||||
--- @param child_names string[]
|
--- @param child_names string[]
|
||||||
local function expand_to(node, child_names)
|
local function expand_to(node, child_names)
|
||||||
if #child_names == 0 then return end
|
if #child_names == 0 then return end
|
||||||
@@ -310,7 +310,7 @@ local function _render_in_buffer(opts)
|
|||||||
--
|
--
|
||||||
local renderer = Renderer.new(opts.bufnr)
|
local renderer = Renderer.new(opts.bufnr)
|
||||||
tracker.create_effect(function()
|
tracker.create_effect(function()
|
||||||
--- @type { tree: u.examples.FsDir, path_to_node: table<string, u.examples.FsNode> }
|
--- @type { tree: FsDir; path_to_node: table<string, FsNode> }
|
||||||
local tree_inf = s_tree_inf:get()
|
local tree_inf = s_tree_inf:get()
|
||||||
local tree = tree_inf.tree
|
local tree = tree_inf.tree
|
||||||
|
|
||||||
@@ -329,7 +329,7 @@ local function _render_in_buffer(opts)
|
|||||||
|
|
||||||
--- Since the filesystem is a recursive tree of nodes, we need to
|
--- Since the filesystem is a recursive tree of nodes, we need to
|
||||||
--- recursively render each node. This function does just that:
|
--- recursively render each node. This function does just that:
|
||||||
--- @param node u.examples.FsNode
|
--- @param node FsNode
|
||||||
--- @param level number
|
--- @param level number
|
||||||
local function render_node(node, level)
|
local function render_node(node, level)
|
||||||
local name = vim.fs.basename(node.path)
|
local name = vim.fs.basename(node.path)
|
||||||
@@ -407,14 +407,14 @@ end
|
|||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
--- @type {
|
--- @type {
|
||||||
--- bufnr: number,
|
--- bufnr: number;
|
||||||
--- winnr: number,
|
--- winnr: number;
|
||||||
--- controller: { expand: fun(path: string), collapse: fun(path: string) }
|
--- controller: { expand: fun(path: string), collapse: fun(path: string) };
|
||||||
--- } | nil
|
--- } | nil
|
||||||
local current_inf = nil
|
local current_inf = nil
|
||||||
|
|
||||||
--- Show the filetree:
|
--- Show the filetree:
|
||||||
--- @param opts? u.examples.ShowOpts
|
--- @param opts? ShowOpts
|
||||||
function M.show(opts)
|
function M.show(opts)
|
||||||
if current_inf ~= nil then return current_inf.controller end
|
if current_inf ~= nil then return current_inf.controller end
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
@@ -456,7 +456,7 @@ function M.hide()
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Toggle the filetree:
|
--- Toggle the filetree:
|
||||||
--- @param opts? u.examples.ShowOpts
|
--- @param opts? ShowOpts
|
||||||
function M.toggle(opts)
|
function M.toggle(opts)
|
||||||
if current_inf == nil then
|
if current_inf == nil then
|
||||||
M.show(opts)
|
M.show(opts)
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
-- form.lua:
|
|
||||||
--
|
|
||||||
-- This is a runnable example of a form. Open this file in Neovim, and execute
|
|
||||||
-- `:luafile %` to run it. It will create a new buffer to the side, and render
|
|
||||||
-- an interactive form. Edit the "inputs" between the `[...]` brackets, and
|
|
||||||
-- watch the buffer react immediately to your changes.
|
|
||||||
--
|
|
||||||
|
|
||||||
local Renderer = require('u.renderer').Renderer
|
|
||||||
local h = require('u.renderer').h
|
|
||||||
local tracker = require 'u.tracker'
|
|
||||||
|
|
||||||
-- Create a new, temporary, buffer to the side:
|
|
||||||
vim.cmd.vnew()
|
|
||||||
vim.bo.buftype = 'nofile'
|
|
||||||
vim.bo.bufhidden = 'wipe'
|
|
||||||
vim.bo.buflisted = false
|
|
||||||
local renderer = Renderer.new()
|
|
||||||
|
|
||||||
-- Create two signals:
|
|
||||||
local s_name = tracker.create_signal 'whoever-you-are'
|
|
||||||
local s_age = tracker.create_signal 'ideally-a-number'
|
|
||||||
|
|
||||||
-- We can create derived information from the signals above. Say we want to do
|
|
||||||
-- some validation on the input for `age`: we can do that with a memo:
|
|
||||||
local s_age_info = tracker.create_memo(function()
|
|
||||||
local age_raw = s_age:get()
|
|
||||||
local age_digits = age_raw:match '^%s*(%d+)%s*$'
|
|
||||||
local age_n = age_digits and tonumber(age_digits) or nil
|
|
||||||
return {
|
|
||||||
type = age_n and 'number' or 'string',
|
|
||||||
raw = age_raw,
|
|
||||||
n = age_n,
|
|
||||||
n1 = age_n and age_n + 1 or nil,
|
|
||||||
}
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- This is the render effect that depends on the signals created above. This
|
|
||||||
-- will re-run every time one of the signals changes.
|
|
||||||
tracker.create_effect(function()
|
|
||||||
local name = s_name:get()
|
|
||||||
local age = s_age:get()
|
|
||||||
local age_info = s_age_info:get()
|
|
||||||
|
|
||||||
-- Each time the signals change, we re-render the buffer:
|
|
||||||
renderer:render {
|
|
||||||
h.Type({}, '# Form Example'),
|
|
||||||
'\n\n',
|
|
||||||
|
|
||||||
-- We can also listen for when specific locations in the buffer change, on
|
|
||||||
-- a tag-by-tag basis. This gives us two-way data-binding between the
|
|
||||||
-- buffer and the signals.
|
|
||||||
{
|
|
||||||
'Name: ',
|
|
||||||
h.Structure({
|
|
||||||
on_change = function(text) s_name:set(text) end,
|
|
||||||
}, name),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'\nAge: ',
|
|
||||||
h.Structure({
|
|
||||||
on_change = function(text) s_age:set(text) end,
|
|
||||||
}, age),
|
|
||||||
},
|
|
||||||
|
|
||||||
'\n\n',
|
|
||||||
|
|
||||||
-- Show the values of the signals here, too, so that we can see the
|
|
||||||
-- reactivity in action. If you change the values in the tags above, you
|
|
||||||
-- can see the changes reflected here immediately.
|
|
||||||
{ 'Hello, "', name, '"!' },
|
|
||||||
|
|
||||||
--
|
|
||||||
-- A more complex example: we can do much more complex rendering, based on
|
|
||||||
-- the state. For example, if you type different values into the `age`
|
|
||||||
-- field, you can see not only the displayed information change, but also
|
|
||||||
-- the color of the highlights in this section will adapt to the type of
|
|
||||||
-- information that has been detected.
|
|
||||||
--
|
|
||||||
-- If string input is detected, values below are shown in the
|
|
||||||
-- `String`/`ErrorMsg` highlight groups.
|
|
||||||
--
|
|
||||||
-- If number input is detected, values below are shown in the `Number`
|
|
||||||
-- highlight group.
|
|
||||||
--
|
|
||||||
-- If a valid number is entered, then this section also displays how old
|
|
||||||
-- you willl be next year (`n + 1`).
|
|
||||||
--
|
|
||||||
|
|
||||||
'\n\n',
|
|
||||||
h.Type({}, '## Computed Information (derived from `age`)'),
|
|
||||||
'\n\n',
|
|
||||||
{
|
|
||||||
'Type: ',
|
|
||||||
h('text', {
|
|
||||||
hl = age_info.type == 'number' and 'Number' or 'String',
|
|
||||||
}, age_info.type),
|
|
||||||
},
|
|
||||||
{ '\nRaw input: ', h.String({}, '"' .. age_info.raw .. '"') },
|
|
||||||
{
|
|
||||||
'\nCurrent age: ',
|
|
||||||
age_info.n
|
|
||||||
-- Show the age:
|
|
||||||
and h.Number({}, tostring(age_info.n))
|
|
||||||
-- Show an error-placeholder if the age is invalid:
|
|
||||||
or h.ErrorMsg({}, '(?)'),
|
|
||||||
},
|
|
||||||
|
|
||||||
-- This part is shown conditionally, i.e., only if the age next year can be
|
|
||||||
-- computed:
|
|
||||||
age_info.n1
|
|
||||||
and {
|
|
||||||
'\nAge next year: ',
|
|
||||||
h.Number({}, tostring(age_info.n1)),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
end)
|
|
||||||
@@ -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')
|
--- kind: number;
|
||||||
vim.api.nvim_create_autocmd('VimResized', {
|
--- id: number;
|
||||||
callback = function()
|
--- text: string;
|
||||||
local new_dim = utils.get_editor_dimensions()
|
|
||||||
S_EDITOR_DIMENSIONS:set(new_dim)
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
--- @alias u.examples.Notification {
|
|
||||||
--- kind: number,
|
|
||||||
--- id: number,
|
|
||||||
--- text: string,
|
|
||||||
--- timer: uv.uv_timer_t
|
|
||||||
--- }
|
--- }
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
--- @type { win: integer, buf: integer, renderer: u.renderer.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.examples.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.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
|
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.examples.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.examples.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
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
local Buffer = require 'u.buffer'
|
|
||||||
local utils = require 'u.utils'
|
local utils = require 'u.utils'
|
||||||
|
local Buffer = require 'u.buffer'
|
||||||
local Renderer = require('u.renderer').Renderer
|
local Renderer = require('u.renderer').Renderer
|
||||||
local h = require('u.renderer').h
|
local h = require('u.renderer').h
|
||||||
local TreeBuilder = require('u.renderer').TreeBuilder
|
local TreeBuilder = require('u.renderer').TreeBuilder
|
||||||
@@ -44,26 +44,26 @@ local function shallow_copy_arr(arr) return vim.iter(arr):totable() end
|
|||||||
-- shortest portion of this function.
|
-- shortest portion of this function.
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
--- @alias u.examples.SelectController<T> {
|
--- @alias SelectController {
|
||||||
--- get_items: fun(): T[],
|
--- get_items: fun(): T[];
|
||||||
--- set_items: fun(items: T[]),
|
--- set_items: fun(items: T[]);
|
||||||
--- set_filter_text: fun(filter_text: string),
|
--- set_filter_text: fun(filter_text: string);
|
||||||
--- get_selected_indices: fun(): number[],
|
--- get_selected_indices: fun(): number[];
|
||||||
--- get_selected_items: fun(): T[],
|
--- get_selected_items: fun(): T[];
|
||||||
--- set_selected_indices: fun(indicies: number[], ephemeral?: boolean),
|
--- set_selected_indices: fun(indicies: number[], ephemeral?: boolean);
|
||||||
--- close: fun()
|
--- close: fun();
|
||||||
--- }
|
--- }
|
||||||
--- @alias u.examples.SelectOpts<T> {
|
--- @alias SelectOpts<T> {
|
||||||
--- items: `T`[],
|
--- items: `T`[];
|
||||||
--- multi?: boolean,
|
--- multi?: boolean;
|
||||||
--- format_item?: (fun(item: T): u.renderer.Tree),
|
--- format_item?: fun(item: T): Tree;
|
||||||
--- on_finish?: (fun(items: T[], indicies: number[])),
|
--- on_finish?: fun(items: T[], indicies: number[]);
|
||||||
--- on_selection_changed?: fun(items: T[], indicies: number[]),
|
--- on_selection_changed?: fun(items: T[], indicies: number[]);
|
||||||
--- mappings?: table<string, fun(select: u.examples.SelectController)>
|
--- mappings?: table<string, fun(select: SelectController)>;
|
||||||
--- }
|
--- }
|
||||||
|
|
||||||
--- @generic T
|
--- @generic T
|
||||||
--- @param opts u.examples.SelectOpts<T>
|
--- @param opts SelectOpts<T>
|
||||||
function M.create_picker(opts) -- {{{
|
function M.create_picker(opts) -- {{{
|
||||||
local is_in_insert_mode = vim.api.nvim_get_mode().mode:sub(1, 1) == 'i'
|
local is_in_insert_mode = vim.api.nvim_get_mode().mode:sub(1, 1) == 'i'
|
||||||
local stopinsert = not is_in_insert_mode
|
local stopinsert = not is_in_insert_mode
|
||||||
@@ -326,7 +326,7 @@ function M.create_picker(opts) -- {{{
|
|||||||
safe_wrap(function()
|
safe_wrap(function()
|
||||||
local items = s_items:get()
|
local items = s_items:get()
|
||||||
local selected_indices = s_selected_indices: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 filtered_items = s_filtered_items:get()
|
||||||
local cursor_index = s_cursor_index:get()
|
local cursor_index = s_cursor_index:get()
|
||||||
local indices = shallow_copy_arr(selected_indices)
|
local indices = shallow_copy_arr(selected_indices)
|
||||||
@@ -477,7 +477,7 @@ function M.create_picker(opts) -- {{{
|
|||||||
local selected_indices = s_selected_indices:get()
|
local selected_indices = s_selected_indices:get()
|
||||||
local top_offset = s_top_offset:get()
|
local top_offset = s_top_offset:get()
|
||||||
local cursor_index = s_cursor_index: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()
|
local visible_items = s_visible_items:get()
|
||||||
|
|
||||||
-- The above has to run in the execution context for the signaling to work, but
|
-- The above has to run in the execution context for the signaling to work, but
|
||||||
@@ -557,7 +557,7 @@ function M.create_picker(opts) -- {{{
|
|||||||
return safe_run(function() H.finish(true) end)
|
return safe_run(function() H.finish(true) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return controller --[[@as u.examples.SelectController]]
|
return controller --[[@as SelectController]]
|
||||||
end -- }}}
|
end -- }}}
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
@@ -791,14 +791,14 @@ function M.buffers() -- {{{
|
|||||||
-- ensure that `cwd` ends with a trailing slash:
|
-- ensure that `cwd` ends with a trailing slash:
|
||||||
if cwd[#cwd] ~= '/' then cwd = cwd .. '/' end
|
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 }
|
local bufs = vim.fn.getbufinfo { buflisted = 1 }
|
||||||
|
|
||||||
M.create_picker {
|
M.create_picker {
|
||||||
multi = true,
|
multi = true,
|
||||||
items = bufs,
|
items = bufs,
|
||||||
|
|
||||||
--- @param item { name: string, changed: number, bufnr: number }
|
--- @param item { name: string; changed: number; bufnr: number }
|
||||||
format_item = function(item)
|
format_item = function(item)
|
||||||
local item_name = item.name
|
local item_name = item.name
|
||||||
if item_name == '' then item_name = '[No Name]' end
|
if item_name == '' then item_name = '[No Name]' end
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
local vim_repeat = require 'u.repeat'
|
||||||
local CodeWriter = require 'u.codewriter'
|
local CodeWriter = require 'u.codewriter'
|
||||||
local Range = require 'u.range'
|
local Range = require 'u.range'
|
||||||
local vim_repeat = require 'u.repeat'
|
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
local vim_repeat = require 'u.repeat'
|
||||||
|
local Range = require 'u.range'
|
||||||
local Buffer = require 'u.buffer'
|
local Buffer = require 'u.buffer'
|
||||||
local CodeWriter = require 'u.codewriter'
|
local CodeWriter = require 'u.codewriter'
|
||||||
local Range = require 'u.range'
|
|
||||||
local vim_repeat = require 'u.repeat'
|
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
@@ -21,10 +21,10 @@ local surrounds = {
|
|||||||
['`'] = { left = '`', right = '`' },
|
['`'] = { left = '`', right = '`' },
|
||||||
}
|
}
|
||||||
|
|
||||||
--- @type { left: string, right: string } | nil
|
--- @type { left: string; right: string } | nil
|
||||||
local CACHED_BOUNDS = nil
|
local CACHED_BOUNDS = nil
|
||||||
|
|
||||||
--- @return { left: string, right: string }|nil
|
--- @return { left: string; right: string }|nil
|
||||||
local function prompt_for_bounds()
|
local function prompt_for_bounds()
|
||||||
if vim_repeat.is_repeating() then
|
if vim_repeat.is_repeating() then
|
||||||
-- If we are repeating, we don't want to prompt for bounds, because
|
-- If we are repeating, we don't want to prompt for bounds, because
|
||||||
@@ -55,7 +55,7 @@ local function prompt_for_bounds()
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- @param range u.Range
|
--- @param range u.Range
|
||||||
--- @param bounds { left: string, right: string }
|
--- @param bounds { left: string; right: string }
|
||||||
local function do_surround(range, bounds)
|
local function do_surround(range, bounds)
|
||||||
local left = bounds.left
|
local left = bounds.left
|
||||||
local right = bounds.right
|
local right = bounds.right
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
local Buffer = require 'u.buffer'
|
local txtobj = require 'u.txtobj'
|
||||||
local Pos = require 'u.pos'
|
local Pos = require 'u.pos'
|
||||||
local Range = require 'u.range'
|
local Range = require 'u.range'
|
||||||
local txtobj = require 'u.txtobj'
|
local Buffer = require 'u.buffer'
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
|||||||
Submodule library/busted deleted from 5ed85d0e01
@@ -2,7 +2,7 @@ local Range = require 'u.range'
|
|||||||
local Renderer = require('u.renderer').Renderer
|
local Renderer = require('u.renderer').Renderer
|
||||||
|
|
||||||
--- @class u.Buffer
|
--- @class u.Buffer
|
||||||
--- @field bufnr integer
|
--- @field bufnr number
|
||||||
--- @field b vim.var_accessor
|
--- @field b vim.var_accessor
|
||||||
--- @field bo vim.bo
|
--- @field bo vim.bo
|
||||||
--- @field renderer u.renderer.Renderer
|
--- @field renderer u.renderer.Renderer
|
||||||
@@ -62,22 +62,19 @@ end
|
|||||||
function Buffer:lines(start, stop) return Range.from_lines(self.bufnr, start, stop) end
|
function Buffer:lines(start, stop) return Range.from_lines(self.bufnr, start, stop) end
|
||||||
|
|
||||||
--- @param motion string
|
--- @param motion string
|
||||||
--- @param opts? { contains_cursor?: boolean, pos?: u.Pos }
|
--- @param opts? { contains_cursor?: boolean; pos?: u.Pos }
|
||||||
function Buffer:motion(motion, opts)
|
function Buffer:motion(motion, opts)
|
||||||
opts = vim.tbl_extend('force', opts or {}, { bufnr = self.bufnr })
|
opts = vim.tbl_extend('force', opts or {}, { bufnr = self.bufnr })
|
||||||
return Range.from_motion(motion, opts)
|
return Range.from_motion(motion, opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param event vim.api.keyset.events|vim.api.keyset.events[]
|
--- @param event string|string[]
|
||||||
--- @diagnostic disable-next-line: undefined-doc-name
|
--- @diagnostic disable-next-line: undefined-doc-name
|
||||||
--- @param opts vim.api.keyset.create_autocmd
|
--- @param opts vim.api.keyset.create_autocmd
|
||||||
function Buffer:autocmd(event, opts)
|
function Buffer:autocmd(event, opts)
|
||||||
vim.api.nvim_create_autocmd(event, vim.tbl_extend('force', opts, { buffer = self.bufnr }))
|
vim.api.nvim_create_autocmd(event, vim.tbl_extend('force', opts, { buffer = self.bufnr }))
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param fn function
|
|
||||||
function Buffer:call(fn) return vim.api.nvim_buf_call(self.bufnr, fn) end
|
|
||||||
|
|
||||||
--- @param tree u.renderer.Tree
|
--- @param tree u.renderer.Tree
|
||||||
function Buffer:render(tree) return self.renderer:render(tree) end
|
function Buffer:render(tree) return self.renderer:render(tree) end
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ end
|
|||||||
--- @param line string
|
--- @param line string
|
||||||
--- @param bufnr? number
|
--- @param bufnr? number
|
||||||
function CodeWriter.from_line(line, bufnr)
|
function CodeWriter.from_line(line, bufnr)
|
||||||
if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
|
if bufnr == nil then bufnr = vim.api.nvim_get_current_buf() end
|
||||||
|
|
||||||
local ws = line:match '^%s*'
|
local ws = line:match '^%s*'
|
||||||
local expandtab = vim.api.nvim_get_option_value('expandtab', { buf = bufnr })
|
local expandtab = vim.api.nvim_get_option_value('expandtab', { buf = bufnr })
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
local Pos = require 'u.pos'
|
|
||||||
|
|
||||||
---@class u.Extmark
|
|
||||||
---@field bufnr integer
|
|
||||||
---@field id integer
|
|
||||||
---@field nsid integer
|
|
||||||
local Extmark = {}
|
|
||||||
Extmark.__index = Extmark
|
|
||||||
|
|
||||||
--- @param bufnr integer
|
|
||||||
--- @param nsid integer
|
|
||||||
--- @param id integer
|
|
||||||
function Extmark.new(bufnr, nsid, id)
|
|
||||||
return setmetatable({
|
|
||||||
bufnr = bufnr,
|
|
||||||
nsid = nsid,
|
|
||||||
id = id,
|
|
||||||
}, Extmark)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- @param range u.Range
|
|
||||||
--- @param nsid integer
|
|
||||||
function Extmark.from_range(range, nsid)
|
|
||||||
local r = range:to_charwise()
|
|
||||||
local stop = r.stop or r.start
|
|
||||||
local end_row = stop.lnum - 1
|
|
||||||
local end_col = stop.col
|
|
||||||
if range.mode == 'V' then
|
|
||||||
end_row = end_row + 1
|
|
||||||
end_col = 0
|
|
||||||
end
|
|
||||||
local id = vim.api.nvim_buf_set_extmark(r.start.bufnr, nsid, r.start.lnum - 1, r.start.col - 1, {
|
|
||||||
right_gravity = false,
|
|
||||||
end_right_gravity = true,
|
|
||||||
end_row = end_row,
|
|
||||||
end_col = end_col,
|
|
||||||
})
|
|
||||||
return Extmark.new(r.start.bufnr, nsid, id)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Extmark:range()
|
|
||||||
local Range = require 'u.range'
|
|
||||||
|
|
||||||
local raw_extmark =
|
|
||||||
vim.api.nvim_buf_get_extmark_by_id(self.bufnr, self.nsid, self.id, { details = true })
|
|
||||||
local start_row0, start_col0, details = unpack(raw_extmark)
|
|
||||||
|
|
||||||
--- @type u.Pos
|
|
||||||
local start = Pos.from00(self.bufnr, start_row0, start_col0)
|
|
||||||
--- @type u.Pos?
|
|
||||||
local stop = details
|
|
||||||
and details.end_row
|
|
||||||
and details.end_col
|
|
||||||
and Pos.from01(self.bufnr, details.end_row, details.end_col)
|
|
||||||
|
|
||||||
local n_buf_lines = vim.api.nvim_buf_line_count(self.bufnr)
|
|
||||||
if stop and stop.lnum > n_buf_lines then
|
|
||||||
stop.lnum = n_buf_lines
|
|
||||||
stop = stop:eol()
|
|
||||||
end
|
|
||||||
if stop and stop.col == 0 then
|
|
||||||
stop.col = 1
|
|
||||||
stop = stop:next(-1)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Range.new(start, stop, 'v')
|
|
||||||
end
|
|
||||||
|
|
||||||
function Extmark:delete() vim.api.nvim_buf_del_extmark(self.bufnr, self.nsid, self.id) end
|
|
||||||
|
|
||||||
return Extmark
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
local LOG_ROOT = vim.fs.joinpath(vim.fn.stdpath 'cache', 'u.log')
|
|
||||||
|
|
||||||
--- @params name string
|
--- @params name string
|
||||||
function M.file_for_name(name) return vim.fs.joinpath(LOG_ROOT, name .. '.log.jsonl') end
|
function M.file_for_name(name)
|
||||||
|
return vim.fs.joinpath(vim.fn.stdpath 'cache', 'u.log', name .. '.log.jsonl')
|
||||||
|
end
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- Logger class
|
-- Logger class
|
||||||
@@ -11,6 +11,7 @@ function M.file_for_name(name) return vim.fs.joinpath(LOG_ROOT, name .. '.log.js
|
|||||||
|
|
||||||
--- @class u.Logger
|
--- @class u.Logger
|
||||||
--- @field name string
|
--- @field name string
|
||||||
|
--- @field private fd number
|
||||||
local Logger = {}
|
local Logger = {}
|
||||||
Logger.__index = Logger
|
Logger.__index = Logger
|
||||||
M.Logger = Logger
|
M.Logger = Logger
|
||||||
@@ -19,7 +20,10 @@ M.Logger = Logger
|
|||||||
function Logger.new(name)
|
function Logger.new(name)
|
||||||
local file_path = M.file_for_name(name)
|
local file_path = M.file_for_name(name)
|
||||||
vim.fn.mkdir(vim.fs.dirname(file_path), 'p')
|
vim.fn.mkdir(vim.fs.dirname(file_path), 'p')
|
||||||
local self = setmetatable({ name = name }, Logger)
|
local self = setmetatable({
|
||||||
|
name = name,
|
||||||
|
fd = (vim.uv or vim.loop).fs_open(file_path, 'a', tonumber('644', 8)),
|
||||||
|
}, Logger)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -28,12 +32,10 @@ end
|
|||||||
function Logger:write(level, ...)
|
function Logger:write(level, ...)
|
||||||
local data = { ... }
|
local data = { ... }
|
||||||
if #data == 1 then data = data[1] end
|
if #data == 1 then data = data[1] end
|
||||||
local f = assert(io.open(M.file_for_name(self.name), 'a'), 'could not open file')
|
(vim.uv or vim.loop).fs_write(
|
||||||
assert(
|
self.fd,
|
||||||
f:write(vim.json.encode { ts = os.date(), level = level, data = data } .. '\n'),
|
vim.json.encode { ts = os.date(), level = level, data = data } .. '\n'
|
||||||
'could not write to file'
|
|
||||||
)
|
)
|
||||||
f:close()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Logger:trace(...) self:write('INFO', ...) end
|
function Logger:trace(...) self:write('INFO', ...) end
|
||||||
@@ -62,12 +64,6 @@ function M.setup()
|
|||||||
vim.cmd.terminal('tail -f "' .. log_file_path .. '"')
|
vim.cmd.terminal('tail -f "' .. log_file_path .. '"')
|
||||||
vim.cmd.startinsert()
|
vim.cmd.startinsert()
|
||||||
end, { nargs = '*' })
|
end, { nargs = '*' })
|
||||||
|
|
||||||
vim.api.nvim_create_user_command(
|
|
||||||
'Logroot',
|
|
||||||
function() vim.api.nvim_echo({ { LOG_ROOT } }, false, {}) end,
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ local __U__OpKeymapOpFunc_rhs = nil
|
|||||||
|
|
||||||
--- This is the global utility function used for operatorfunc
|
--- This is the global utility function used for operatorfunc
|
||||||
--- in opkeymap
|
--- in opkeymap
|
||||||
|
--- @type nil|fun(range: u.Range): fun():any|nil
|
||||||
--- @param ty 'line'|'char'|'block'
|
--- @param ty 'line'|'char'|'block'
|
||||||
-- selene: allow(unused_variable)
|
-- selene: allow(unused_variable)
|
||||||
function _G.__U__OpKeymapOpFunc(ty)
|
function _G.__U__OpKeymapOpFunc(ty)
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ local function line_text(bufnr, lnum)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- @class u.Pos
|
--- @class u.Pos
|
||||||
--- @field bufnr integer buffer number
|
--- @field bufnr number buffer number
|
||||||
--- @field lnum integer 1-based line index
|
--- @field lnum number 1-based line index
|
||||||
--- @field col integer 1-based column index
|
--- @field col number 1-based column index
|
||||||
--- @field off number
|
--- @field off number
|
||||||
local Pos = {}
|
local Pos = {}
|
||||||
Pos.__index = Pos
|
Pos.__index = Pos
|
||||||
@@ -31,33 +31,16 @@ end
|
|||||||
function Pos.new(bufnr, lnum, col, off)
|
function Pos.new(bufnr, lnum, col, off)
|
||||||
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
|
||||||
if off == nil then off = 0 end
|
if off == nil then off = 0 end
|
||||||
--- @type u.Pos
|
local pos = {
|
||||||
return setmetatable({
|
|
||||||
bufnr = bufnr,
|
bufnr = bufnr,
|
||||||
lnum = lnum,
|
lnum = lnum,
|
||||||
col = col,
|
col = col,
|
||||||
off = off,
|
off = off,
|
||||||
}, Pos)
|
}
|
||||||
|
setmetatable(pos, Pos)
|
||||||
|
return pos
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param bufnr? number
|
|
||||||
--- @param lnum0 number 1-based
|
|
||||||
--- @param col0 number 1-based
|
|
||||||
--- @param off? number
|
|
||||||
function Pos.from00(bufnr, lnum0, col0, off) return Pos.new(bufnr, lnum0 + 1, col0 + 1, off) end
|
|
||||||
|
|
||||||
--- @param bufnr? number
|
|
||||||
--- @param lnum0 number 1-based
|
|
||||||
--- @param col1 number 1-based
|
|
||||||
--- @param off? number
|
|
||||||
function Pos.from01(bufnr, lnum0, col1, off) return Pos.new(bufnr, lnum0 + 1, col1, off) end
|
|
||||||
|
|
||||||
--- @param bufnr? number
|
|
||||||
--- @param lnum1 number 1-based
|
|
||||||
--- @param col0 number 1-based
|
|
||||||
--- @param off? number
|
|
||||||
function Pos.from10(bufnr, lnum1, col0, off) return Pos.new(bufnr, lnum1, col0 + 1, off) end
|
|
||||||
|
|
||||||
function Pos.invalid() return Pos.new(0, 0, 0, 0) end
|
function Pos.invalid() return Pos.new(0, 0, 0, 0) end
|
||||||
|
|
||||||
function Pos.__lt(a, b) return a.lnum < b.lnum or (a.lnum == b.lnum and a.col < b.col) end
|
function Pos.__lt(a, b) return a.lnum < b.lnum or (a.lnum == b.lnum and a.col < b.col) end
|
||||||
@@ -84,15 +67,6 @@ function Pos.__sub(x, y)
|
|||||||
return x:next(-y)
|
return x:next(-y)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param bufnr number
|
|
||||||
--- @param lnum number
|
|
||||||
function Pos.from_eol(bufnr, lnum)
|
|
||||||
if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
|
|
||||||
local pos = Pos.new(bufnr, lnum, 0)
|
|
||||||
pos.col = pos:line():len()
|
|
||||||
return pos
|
|
||||||
end
|
|
||||||
|
|
||||||
--- @param name string
|
--- @param name string
|
||||||
--- @return u.Pos
|
--- @return u.Pos
|
||||||
function Pos.from_pos(name)
|
function Pos.from_pos(name)
|
||||||
@@ -122,16 +96,9 @@ end
|
|||||||
|
|
||||||
function Pos:as_vim() return { self.bufnr, self.lnum, self.col, self.off } end
|
function Pos:as_vim() return { self.bufnr, self.lnum, self.col, self.off } end
|
||||||
|
|
||||||
function Pos:eol() return Pos.from_eol(self.bufnr, self.lnum) end
|
|
||||||
|
|
||||||
--- @param pos string
|
--- @param pos string
|
||||||
function Pos:save_to_pos(pos) vim.fn.setpos(pos, { self.bufnr, self.lnum, self.col, self.off }) end
|
function Pos:save_to_pos(pos) vim.fn.setpos(pos, { self.bufnr, self.lnum, self.col, self.off }) end
|
||||||
|
|
||||||
--- @param winnr? integer
|
|
||||||
function Pos:save_to_cursor(winnr)
|
|
||||||
vim.api.nvim_win_set_cursor(winnr or 0, { self.lnum, self.col - 1 })
|
|
||||||
end
|
|
||||||
|
|
||||||
--- @param mark string
|
--- @param mark string
|
||||||
function Pos:save_to_mark(mark)
|
function Pos:save_to_mark(mark)
|
||||||
local p = self:as_real()
|
local p = self:as_real()
|
||||||
|
|||||||
167
lua/u/range.lua
167
lua/u/range.lua
@@ -1,9 +1,14 @@
|
|||||||
local Extmark = require 'u.extmark'
|
|
||||||
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'
|
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
|
||||||
--- @field stop u.Pos|nil
|
--- @field stop u.Pos|nil
|
||||||
@@ -85,6 +90,28 @@ function Range.from_marks(lpos, rpos)
|
|||||||
return Range.new(start, stop, mode)
|
return Range.new(start, stop, mode)
|
||||||
end
|
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
|
--- @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
|
||||||
@@ -112,12 +139,12 @@ function Range.from_lines(bufnr, start_line, stop_line)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- @param motion string
|
--- @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
|
--- @return u.Range|nil
|
||||||
function Range.from_motion(motion, opts)
|
function Range.from_motion(motion, opts)
|
||||||
-- Options handling:
|
-- Options handling:
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
if opts.bufnr == nil or opts.bufnr == 0 then opts.bufnr = vim.api.nvim_get_current_buf() end
|
if opts.bufnr == nil then opts.bufnr = vim.api.nvim_get_current_buf() end
|
||||||
if opts.contains_cursor == nil then opts.contains_cursor = false end
|
if opts.contains_cursor == nil then opts.contains_cursor = false end
|
||||||
if opts.user_defined == nil then opts.user_defined = false end
|
if opts.user_defined == nil then opts.user_defined = false end
|
||||||
|
|
||||||
@@ -148,8 +175,6 @@ function Range.from_motion(motion, opts)
|
|||||||
_G.Range__from_motion_opfunc = function(ty)
|
_G.Range__from_motion_opfunc = function(ty)
|
||||||
_G.Range__from_motion_opfunc_captured_range = Range.from_op_func(ty)
|
_G.Range__from_motion_opfunc_captured_range = Range.from_op_func(ty)
|
||||||
end
|
end
|
||||||
local old_eventignore = vim.o.eventignore
|
|
||||||
vim.o.eventignore = 'all'
|
|
||||||
vim.go.operatorfunc = 'v:lua.Range__from_motion_opfunc'
|
vim.go.operatorfunc = 'v:lua.Range__from_motion_opfunc'
|
||||||
vim.cmd {
|
vim.cmd {
|
||||||
cmd = 'normal',
|
cmd = 'normal',
|
||||||
@@ -157,7 +182,6 @@ function Range.from_motion(motion, opts)
|
|||||||
args = { ESC .. 'g@' .. motion },
|
args = { ESC .. 'g@' .. motion },
|
||||||
mods = { silent = true },
|
mods = { silent = true },
|
||||||
}
|
}
|
||||||
vim.o.eventignore = old_eventignore
|
|
||||||
end)
|
end)
|
||||||
local captured_range = _G.Range__from_motion_opfunc_captured_range
|
local captured_range = _G.Range__from_motion_opfunc_captured_range
|
||||||
|
|
||||||
@@ -198,26 +222,6 @@ function Range.from_motion(motion, opts)
|
|||||||
return captured_range
|
return captured_range
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param opts? { contains_cursor?: boolean }
|
|
||||||
function Range.from_tsquery_caps(bufnr, query, opts)
|
|
||||||
opts = opts or { contains_cursor = true }
|
|
||||||
|
|
||||||
local ranges = Range.from_buf_text(bufnr):tsquery(query)
|
|
||||||
if not ranges then return end
|
|
||||||
if not opts.contains_cursor then return ranges end
|
|
||||||
|
|
||||||
local cursor = Pos.from_pos '.'
|
|
||||||
return vim.tbl_map(function(cap_ranges)
|
|
||||||
return vim
|
|
||||||
.iter(cap_ranges)
|
|
||||||
:filter(
|
|
||||||
--- @param r u.Range
|
|
||||||
function(r) return r:contains(cursor) end
|
|
||||||
)
|
|
||||||
:totable()
|
|
||||||
end, ranges)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Get range information from the currently selected visual text.
|
--- Get range information from the currently selected visual text.
|
||||||
--- Note: from within a command mapping or an opfunc, use other specialized
|
--- Note: from within a command mapping or an opfunc, use other specialized
|
||||||
--- utilities, such as:
|
--- utilities, such as:
|
||||||
@@ -245,22 +249,19 @@ end
|
|||||||
--- @param args unknown
|
--- @param args unknown
|
||||||
--- @return u.Range|nil
|
--- @return u.Range|nil
|
||||||
function Range.from_cmd_args(args)
|
function Range.from_cmd_args(args)
|
||||||
if args.range == 0 then return nil end
|
|
||||||
|
|
||||||
local bufnr = vim.api.nvim_get_current_buf()
|
|
||||||
if args.range == 1 then
|
|
||||||
return Range.new(Pos.new(bufnr, args.line1, 1), Pos.new(bufnr, args.line1, Pos.MAX_COL), 'V')
|
|
||||||
end
|
|
||||||
|
|
||||||
local is_visual = vim.fn.histget('cmd', -1):sub(1, 5) == [['<,'>]]
|
|
||||||
--- @type 'v'|'V'
|
--- @type 'v'|'V'
|
||||||
local mode = is_visual and vim.fn.visualmode() or 'V'
|
local mode
|
||||||
|
--- @type nil|u.Pos
|
||||||
if is_visual then
|
local start
|
||||||
return Range.new(Pos.from_pos "'<", Pos.from_pos "'>", mode)
|
local stop
|
||||||
|
if args.range == 0 then
|
||||||
|
return nil
|
||||||
else
|
else
|
||||||
return Range.new(Pos.new(bufnr, args.line1, 1), Pos.new(bufnr, args.line2, Pos.MAX_COL), mode)
|
start = Pos.from_pos "'<"
|
||||||
|
stop = Pos.from_pos "'>"
|
||||||
|
mode = stop:is_col_max() and 'V' or 'v'
|
||||||
end
|
end
|
||||||
|
return Range.new(start, stop, mode)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Range.find_nearest_brackets()
|
function Range.find_nearest_brackets()
|
||||||
@@ -348,18 +349,41 @@ end
|
|||||||
--- @param left string
|
--- @param left string
|
||||||
--- @param right string
|
--- @param right string
|
||||||
function Range:save_to_pos(left, right)
|
function Range:save_to_pos(left, right)
|
||||||
self.start:save_to_pos(left);
|
if self:is_empty() then
|
||||||
(self:is_empty() and self.start or self.stop):save_to_pos(right)
|
self.start:save_to_pos(left)
|
||||||
|
self.start:save_to_pos(right)
|
||||||
|
else
|
||||||
|
self.start:save_to_pos(left)
|
||||||
|
self.stop:save_to_pos(right)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param left string
|
--- @param left string
|
||||||
--- @param right string
|
--- @param right string
|
||||||
function Range:save_to_marks(left, right)
|
function Range:save_to_marks(left, right)
|
||||||
self.start:save_to_mark(left);
|
if self:is_empty() then
|
||||||
(self:is_empty() and self.start or self.stop):save_to_mark(right)
|
self.start:save_to_mark(left)
|
||||||
|
self.start:save_to_mark(right)
|
||||||
|
else
|
||||||
|
self.start:save_to_mark(left)
|
||||||
|
self.stop:save_to_mark(right)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Range:save_to_extmark() return Extmark.from_range(self, NS) 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
|
||||||
@@ -376,44 +400,12 @@ function Range:set_visual_selection()
|
|||||||
end
|
end
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- Text access/manipulation utilities:
|
-- Range.from_* functions:
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
--- @param query string
|
--------------------------------------------------------------------------------
|
||||||
function Range:tsquery(query)
|
-- Text access/manipulation utilities:
|
||||||
local bufnr = self.start.bufnr
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
local lang = vim.treesitter.language.get_lang(vim.bo[bufnr].filetype)
|
|
||||||
if lang == nil then return end
|
|
||||||
local parser = vim.treesitter.get_parser(bufnr, lang)
|
|
||||||
if parser == nil then return end
|
|
||||||
local tree = parser:parse()[1]
|
|
||||||
if tree == nil then return end
|
|
||||||
|
|
||||||
local root = tree:root()
|
|
||||||
local q = vim.treesitter.query.parse(lang, query)
|
|
||||||
--- @type table<string, u.Range[]>
|
|
||||||
local ranges = {}
|
|
||||||
for id, match, _meta in
|
|
||||||
q:iter_captures(root, bufnr, self.start.lnum - 1, (self.stop or self.start).lnum)
|
|
||||||
do
|
|
||||||
local start_row0, start_col0, stop_row0, stop_col0 = match:range()
|
|
||||||
local range = Range.new(
|
|
||||||
Pos.new(bufnr, start_row0 + 1, start_col0 + 1),
|
|
||||||
Pos.new(bufnr, stop_row0 + 1, stop_col0),
|
|
||||||
'v'
|
|
||||||
)
|
|
||||||
if range.stop.lnum > vim.api.nvim_buf_line_count(bufnr) then
|
|
||||||
range.stop = range.stop:must_next(-1)
|
|
||||||
end
|
|
||||||
|
|
||||||
local capture_name = q.captures[id]
|
|
||||||
if not ranges[capture_name] then ranges[capture_name] = {} end
|
|
||||||
if self:contains(range) then table.insert(ranges[capture_name], range) end
|
|
||||||
end
|
|
||||||
|
|
||||||
return ranges
|
|
||||||
end
|
|
||||||
|
|
||||||
function Range:length()
|
function Range:length()
|
||||||
if self:is_empty() then return 0 end
|
if self:is_empty() then return 0 end
|
||||||
@@ -513,7 +505,7 @@ function Range:text() return vim.fn.join(self:lines(), '\n') end
|
|||||||
|
|
||||||
--- @param l number
|
--- @param l number
|
||||||
-- luacheck: ignore
|
-- 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)
|
function Range:line(l)
|
||||||
if l < 0 then l = self:line_count() + l + 1 end
|
if l < 0 then l = self:line_count() + l + 1 end
|
||||||
if l > self:line_count() then return end
|
if l > self:line_count() then return end
|
||||||
@@ -655,4 +647,17 @@ 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,
|
||||||
|
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
|
return Range
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,19 +6,19 @@ M.debug = false
|
|||||||
-- class Signal
|
-- class Signal
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
--- @class u.Signal<T>
|
--- @class u.Signal
|
||||||
--- @field name? string
|
--- @field name? string
|
||||||
--- @field private changing boolean
|
--- @field private changing boolean
|
||||||
--- @field private value T
|
--- @field private value any
|
||||||
--- @field private subscribers table<function, boolean>
|
--- @field private subscribers table<function, boolean>
|
||||||
--- @field private on_dispose_callbacks function[]
|
--- @field private on_dispose_callbacks function[]
|
||||||
local Signal = {}
|
local Signal = {}
|
||||||
M.Signal = Signal
|
M.Signal = Signal
|
||||||
Signal.__index = Signal
|
Signal.__index = Signal
|
||||||
|
|
||||||
--- @param value `T`
|
--- @param value any
|
||||||
--- @param name? string
|
--- @param name? string
|
||||||
--- @return u.Signal<T>
|
--- @return u.Signal
|
||||||
function Signal:new(value, name)
|
function Signal:new(value, name)
|
||||||
local obj = setmetatable({
|
local obj = setmetatable({
|
||||||
name = name,
|
name = name,
|
||||||
@@ -30,7 +30,7 @@ function Signal:new(value, name)
|
|||||||
return obj
|
return obj
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param value T
|
--- @param value any
|
||||||
function Signal:set(value)
|
function Signal:set(value)
|
||||||
self.value = value
|
self.value = value
|
||||||
|
|
||||||
@@ -67,12 +67,11 @@ function Signal:set(value)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param value T
|
|
||||||
function Signal:schedule_set(value)
|
function Signal:schedule_set(value)
|
||||||
vim.schedule(function() self:set(value) end)
|
vim.schedule(function() self:set(value) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @return T
|
--- @return any
|
||||||
function Signal:get()
|
function Signal:get()
|
||||||
local ctx = M.ExecutionContext.current()
|
local ctx = M.ExecutionContext.current()
|
||||||
if ctx then ctx:track(self) end
|
if ctx then ctx:track(self) end
|
||||||
@@ -86,8 +85,8 @@ function Signal:update(fn) self:set(fn(self.value)) end
|
|||||||
function Signal:schedule_update(fn) self:schedule_set(fn(self.value)) end
|
function Signal:schedule_update(fn) self:schedule_set(fn(self.value)) end
|
||||||
|
|
||||||
--- @generic U
|
--- @generic U
|
||||||
--- @param fn fun(value: T): `U`
|
--- @param fn fun(value: T): U
|
||||||
--- @return u.Signal<U>
|
--- @return u.Signal --<U>
|
||||||
function Signal:map(fn)
|
function Signal:map(fn)
|
||||||
local mapped_signal = M.create_memo(function()
|
local mapped_signal = M.create_memo(function()
|
||||||
local value = self:get()
|
local value = self:get()
|
||||||
@@ -96,13 +95,13 @@ function Signal:map(fn)
|
|||||||
return mapped_signal
|
return mapped_signal
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @return u.Signal<T>
|
--- @return u.Signal
|
||||||
function Signal:clone()
|
function Signal:clone()
|
||||||
return self:map(function(x) return x end)
|
return self:map(function(x) return x end)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param fn fun(value: T): boolean
|
--- @param fn fun(value: T): boolean
|
||||||
--- @return u.Signal<T>
|
--- @return u.Signal -- <T>
|
||||||
function Signal:filter(fn)
|
function Signal:filter(fn)
|
||||||
local filtered_signal = M.create_signal(nil, self.name and self.name .. ':filtered' or nil)
|
local filtered_signal = M.create_signal(nil, self.name and self.name .. ':filtered' or nil)
|
||||||
local unsubscribe_from_self = self:subscribe(function(value)
|
local unsubscribe_from_self = self:subscribe(function(value)
|
||||||
@@ -113,10 +112,10 @@ function Signal:filter(fn)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- @param ms number
|
--- @param ms number
|
||||||
--- @return u.Signal<T>
|
--- @return u.Signal -- <T>
|
||||||
function Signal:debounce(ms)
|
function Signal:debounce(ms)
|
||||||
local function set_timeout(timeout, callback)
|
local function set_timeout(timeout, callback)
|
||||||
local timer = assert((vim.uv or vim.loop).new_timer(), 'could not create new timer')
|
local timer = (vim.uv or vim.loop).new_timer()
|
||||||
timer:start(timeout, 0, function()
|
timer:start(timeout, 0, function()
|
||||||
timer:stop()
|
timer:stop()
|
||||||
timer:close()
|
timer:close()
|
||||||
@@ -128,7 +127,7 @@ function Signal:debounce(ms)
|
|||||||
local filtered = M.create_signal(self.value, self.name and self.name .. ':debounced' or nil)
|
local filtered = M.create_signal(self.value, self.name and self.name .. ':debounced' or nil)
|
||||||
|
|
||||||
--- @diagnostic disable-next-line: undefined-doc-name
|
--- @diagnostic disable-next-line: undefined-doc-name
|
||||||
--- @type { queued: { value: T, ts: number }[], timer?: uv.uv_timer_t }
|
--- @type { queued: { value: T, ts: number }[]; timer?: uv_timer_t; }
|
||||||
local state = { queued = {}, timer = nil }
|
local state = { queued = {}, timer = nil }
|
||||||
local function clear_timeout()
|
local function clear_timeout()
|
||||||
if state.timer == nil then return end
|
if state.timer == nil then return end
|
||||||
@@ -203,7 +202,6 @@ end
|
|||||||
-- class ExecutionContext
|
-- class ExecutionContext
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
--- @type u.ExecutionContext|nil
|
|
||||||
local CURRENT_CONTEXT = nil
|
local CURRENT_CONTEXT = nil
|
||||||
|
|
||||||
--- @class u.ExecutionContext
|
--- @class u.ExecutionContext
|
||||||
@@ -264,18 +262,16 @@ end
|
|||||||
-- Helpers
|
-- Helpers
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
--- @generic T
|
--- @param value any
|
||||||
--- @param value `T`
|
|
||||||
--- @param name? string
|
--- @param name? string
|
||||||
--- @return u.Signal<T>
|
--- @return u.Signal
|
||||||
function M.create_signal(value, name) return Signal:new(value, name) end
|
function M.create_signal(value, name) return Signal:new(value, name) end
|
||||||
|
|
||||||
--- @generic T
|
--- @param fn function
|
||||||
--- @param fn fun(): `T`
|
|
||||||
--- @param name? string
|
--- @param name? string
|
||||||
--- @return u.Signal
|
--- @return u.Signal
|
||||||
function M.create_memo(fn, name)
|
function M.create_memo(fn, name)
|
||||||
--- @type u.Signal<T> | nil
|
--- @type u.Signal
|
||||||
local result
|
local result
|
||||||
local unsubscribe = M.create_effect(function()
|
local unsubscribe = M.create_effect(function()
|
||||||
local value = fn()
|
local value = fn()
|
||||||
@@ -286,8 +282,8 @@ function M.create_memo(fn, name)
|
|||||||
result = M.create_signal(value, name and ('m.s:' .. name) or nil)
|
result = M.create_signal(value, name and ('m.s:' .. name) or nil)
|
||||||
end
|
end
|
||||||
end, name)
|
end, name)
|
||||||
assert(result):on_dispose(unsubscribe)
|
result:on_dispose(unsubscribe)
|
||||||
return assert(result)
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param fn function
|
--- @param fn function
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ function M.create_delegated_cmd_args(current_args)
|
|||||||
range = current_args.range == 1 and { current_args.line1 }
|
range = current_args.range == 1 and { current_args.line1 }
|
||||||
or current_args.range == 2 and { current_args.line1, current_args.line2 }
|
or current_args.range == 2 and { current_args.line1, current_args.line2 }
|
||||||
or nil,
|
or nil,
|
||||||
count = (current_args.count ~= -1 and current_args.range == 0) and current_args.count or nil,
|
count = current_args.count ~= -1 and current_args.count or nil,
|
||||||
reg = current_args.reg ~= '' and current_args.reg or nil,
|
reg = current_args.reg ~= '' and current_args.reg or nil,
|
||||||
bang = current_args.bang or nil,
|
bang = current_args.bang or nil,
|
||||||
args = #current_args.fargs > 0 and current_args.fargs or nil,
|
args = #current_args.fargs > 0 and current_args.fargs or nil,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
pkgs ?
|
pkgs ?
|
||||||
import
|
import
|
||||||
# nixos-unstable (neovim@0.11.4):
|
# nixpkgs-unstable (neovim@0.11.2):
|
||||||
(fetchTarball {
|
(fetchTarball {
|
||||||
url = "https://github.com/nixos/nixpkgs/archive/0b4defa2584313f3b781240b29d61f6f9f7e0df3.tar.gz";
|
url = "https://github.com/nixos/nixpkgs/archive/f72be405a10668b8b00937b452f2145244103ebc.tar.gz";
|
||||||
sha256 = "0p3rrd8wwlk0iwgzm7frkw1k98ywrh0avi7fqjjk87i68n3inxrs";
|
sha256 = "0m1vnvngpxrawsgg306c9sdhbzsiigjgb03yfbdpa2fsb1fs0zm9";
|
||||||
})
|
})
|
||||||
{ },
|
{ },
|
||||||
}:
|
}:
|
||||||
@@ -12,10 +12,11 @@ pkgs.mkShell {
|
|||||||
packages = [
|
packages = [
|
||||||
pkgs.git
|
pkgs.git
|
||||||
pkgs.gnumake
|
pkgs.gnumake
|
||||||
pkgs.emmylua-check
|
pkgs.lua-language-server
|
||||||
pkgs.lua51Packages.busted
|
pkgs.lua51Packages.busted
|
||||||
pkgs.lua51Packages.luacov
|
pkgs.lua51Packages.luacov
|
||||||
pkgs.lua51Packages.luarocks
|
pkgs.lua51Packages.luarocks
|
||||||
|
pkgs.lua51Packages.nlua
|
||||||
pkgs.neovim
|
pkgs.neovim
|
||||||
pkgs.stylua
|
pkgs.stylua
|
||||||
pkgs.watchexec
|
pkgs.watchexec
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
local Pos = require 'u.pos'
|
|
||||||
local Range = require 'u.range'
|
local Range = require 'u.range'
|
||||||
|
local Pos = require 'u.pos'
|
||||||
local withbuf = loadfile './spec/withbuf.lua'()
|
local withbuf = loadfile './spec/withbuf.lua'()
|
||||||
|
|
||||||
describe('Range', function()
|
describe('Range', function()
|
||||||
@@ -238,128 +238,6 @@ describe('Range', function()
|
|||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('from_tsquery_caps', function()
|
|
||||||
withbuf({
|
|
||||||
'-- a comment',
|
|
||||||
'',
|
|
||||||
'function foo(bar) end',
|
|
||||||
'',
|
|
||||||
'-- a middle comment',
|
|
||||||
'',
|
|
||||||
'function bar(baz) end',
|
|
||||||
'',
|
|
||||||
'-- another comment',
|
|
||||||
}, function()
|
|
||||||
vim.cmd.setfiletype 'lua'
|
|
||||||
|
|
||||||
--- @param contains_cursor? boolean
|
|
||||||
local function get_caps(contains_cursor)
|
|
||||||
if contains_cursor == nil then contains_cursor = true end
|
|
||||||
return (Range.from_tsquery_caps(
|
|
||||||
0,
|
|
||||||
'(function_declaration) @f',
|
|
||||||
{ contains_cursor = contains_cursor }
|
|
||||||
)).f or {}
|
|
||||||
end
|
|
||||||
|
|
||||||
local caps = get_caps(false)
|
|
||||||
assert.are.same(#caps, 2)
|
|
||||||
assert.are.same(vim.iter(caps):map(function(c) return c:text() end):totable(), {
|
|
||||||
'function foo(bar) end',
|
|
||||||
'function bar(baz) end',
|
|
||||||
})
|
|
||||||
|
|
||||||
Pos.new(0, 1, 1):save_to_pos '.'
|
|
||||||
caps = get_caps()
|
|
||||||
assert.are.same(#caps, 0)
|
|
||||||
|
|
||||||
Pos.new(0, 3, 18):save_to_pos '.'
|
|
||||||
caps = get_caps()
|
|
||||||
assert.are.same(#caps, 1)
|
|
||||||
assert.are.same(caps[1]:text(), 'function foo(bar) end')
|
|
||||||
|
|
||||||
Pos.new(0, 5, 1):save_to_pos '.'
|
|
||||||
caps = get_caps()
|
|
||||||
assert.are.same(#caps, 0)
|
|
||||||
|
|
||||||
Pos.new(0, 7, 1):save_to_pos '.'
|
|
||||||
caps = get_caps()
|
|
||||||
assert.are.same(#caps, 1)
|
|
||||||
assert.are.same(caps[1]:text(), 'function bar(baz) end')
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('from_tsquery_caps with string array filter', function()
|
|
||||||
withbuf({
|
|
||||||
'{',
|
|
||||||
' sample_key1 = "sample-value1",',
|
|
||||||
' sample_key2 = "sample-value2",',
|
|
||||||
'}',
|
|
||||||
}, function()
|
|
||||||
vim.cmd.setfiletype 'lua'
|
|
||||||
|
|
||||||
-- Place cursor in "sample-value1"
|
|
||||||
Pos.new(0, 2, 25):save_to_pos '.'
|
|
||||||
|
|
||||||
-- Query that captures both keys and values in pairs
|
|
||||||
local query = [[
|
|
||||||
(field
|
|
||||||
name: _ @key
|
|
||||||
value: _ @value)
|
|
||||||
]]
|
|
||||||
|
|
||||||
local ranges = Range.from_line(0, 2):tsquery(query)
|
|
||||||
|
|
||||||
-- Should have both @key and @value captures for the first pair only
|
|
||||||
-- (since cursor is in sample-value1)
|
|
||||||
assert(ranges, 'Range should not be nil')
|
|
||||||
assert(ranges.key, 'Range.key should not be nil')
|
|
||||||
assert(ranges.value, 'Range.value should not be nil')
|
|
||||||
|
|
||||||
-- Should have exactly one key and one value
|
|
||||||
assert.are.same(#ranges.key, 1)
|
|
||||||
assert.are.same(#ranges.value, 1)
|
|
||||||
|
|
||||||
-- Check that we got sample-key1 and sample-value1
|
|
||||||
assert.are.same(ranges.key[1]:text(), 'sample_key1')
|
|
||||||
assert.are.same(ranges.value[1]:text(), '"sample-value1"')
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- Make sure this works when the match is on the last line:
|
|
||||||
withbuf({
|
|
||||||
'{sample_key1= "sample-value1",',
|
|
||||||
'sample_key2= "sample-value2"}',
|
|
||||||
}, function()
|
|
||||||
vim.cmd.setfiletype 'lua'
|
|
||||||
|
|
||||||
-- Place cursor in "sample-value1"
|
|
||||||
Pos.new(0, 2, 25):save_to_pos '.'
|
|
||||||
|
|
||||||
-- Query that captures both keys and values in pairs
|
|
||||||
local query = [[
|
|
||||||
(field
|
|
||||||
name: _ @key
|
|
||||||
value: _ @value)
|
|
||||||
]]
|
|
||||||
|
|
||||||
local ranges = Range.from_line(0, 2):tsquery(query)
|
|
||||||
|
|
||||||
-- Should have both @key and @value captures for the first pair only
|
|
||||||
-- (since cursor is in sample-value1)
|
|
||||||
assert(ranges, 'Range should not be nil')
|
|
||||||
assert(ranges.key, 'Range.key should not be nil')
|
|
||||||
assert(ranges.value, 'Range.value should not be nil')
|
|
||||||
|
|
||||||
-- Should have exactly one key and one value
|
|
||||||
assert.are.same(#ranges.key, 1)
|
|
||||||
assert.are.same(#ranges.value, 1)
|
|
||||||
|
|
||||||
-- Check that we got sample-key2 and sample-value2
|
|
||||||
assert.are.same(ranges.key[1]:text(), 'sample_key2')
|
|
||||||
assert.are.same(ranges.value[1]:text(), '"sample-value2"')
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('should get nearest block', function()
|
it('should get nearest block', function()
|
||||||
withbuf({
|
withbuf({
|
||||||
'this is a {',
|
'this is a {',
|
||||||
@@ -440,89 +318,18 @@ describe('Range', function()
|
|||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('from_cmd_args: range=0', function()
|
it('from_cmd_args', function()
|
||||||
local args = { range = 0 }
|
local args = { range = 1 }
|
||||||
withbuf(
|
|
||||||
{ 'line one', 'and line two' },
|
|
||||||
function() assert.are.same(Range.from_cmd_args(args), nil) end
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('from_cmd_args: range=1', function()
|
|
||||||
local args = { range = 1, line1 = 1 }
|
|
||||||
withbuf({ 'line one', 'and line two' }, function()
|
withbuf({ 'line one', 'and line two' }, function()
|
||||||
local a = Pos.new(nil, 1, 1)
|
local a = Pos.new(nil, 1, 1)
|
||||||
local b = Pos.new(nil, 1, Pos.MAX_COL)
|
local b = Pos.new(nil, 2, 2)
|
||||||
|
|
||||||
local range = Range.from_cmd_args(args) --[[@as u.Range]]
|
|
||||||
assert.are.same(range.start, a)
|
|
||||||
assert.are.same(range.stop, b)
|
|
||||||
assert.are.same(range.mode, 'V')
|
|
||||||
assert.are.same(range:text(), 'line one')
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('from_cmd_args: range=2: no-visual', function()
|
|
||||||
local args = { range = 2, line1 = 1, line2 = 2 }
|
|
||||||
withbuf({ 'line one', 'and line two' }, function()
|
|
||||||
local range = Range.from_cmd_args(args) --[[@as u.Range]]
|
|
||||||
assert.are.same(range.start, Pos.new(nil, 1, 1))
|
|
||||||
assert.are.same(range.stop, Pos.new(nil, 2, Pos.MAX_COL))
|
|
||||||
assert.are.same(range.mode, 'V')
|
|
||||||
assert.are.same(range:text(), 'line one\nand line two')
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('from_cmd_args: range=2: visual: linewise', function()
|
|
||||||
local args = { range = 2, line1 = 1, line2 = 2 }
|
|
||||||
withbuf({ 'line one', 'and line two' }, function()
|
|
||||||
local a = Pos.new(nil, 1, 1)
|
|
||||||
local b = Pos.new(nil, 2, Pos.MAX_COL)
|
|
||||||
a:save_to_pos "'<"
|
a:save_to_pos "'<"
|
||||||
b:save_to_pos "'>"
|
b:save_to_pos "'>"
|
||||||
local range = Range.from_cmd_args(args) --[[@as u.Range]]
|
|
||||||
assert.are.same(range.start, a)
|
|
||||||
assert.are.same(range.stop, b)
|
|
||||||
assert.are.same(range.mode, 'V')
|
|
||||||
assert.are.same(range:text(), 'line one\nand line two')
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('from_cmd_args: range=2: visual: charwise', function()
|
local range = Range.from_cmd_args(args)
|
||||||
local args = { range = 2, line1 = 1, line2 = 1 }
|
|
||||||
withbuf({ 'line one', 'and line two' }, function()
|
|
||||||
-- Simulate a visual selection:
|
|
||||||
local a = Pos.new(nil, 1, 1)
|
|
||||||
local b = Pos.new(nil, 1, 4)
|
|
||||||
a:save_to_pos "'<"
|
|
||||||
b:save_to_pos "'>"
|
|
||||||
Range.new(a, b, 'v'):set_visual_selection()
|
|
||||||
assert.are.same(vim.fn.mode(), 'v')
|
|
||||||
|
|
||||||
-- In this simulated setup, we need to force visualmode() to return
|
|
||||||
-- 'v' and histget() to return [['<,'>]]:
|
|
||||||
|
|
||||||
-- visualmode()
|
|
||||||
local orig_visualmode = vim.fn.visualmode
|
|
||||||
--- @diagnostic disable-next-line: duplicate-set-field
|
|
||||||
function vim.fn.visualmode() return 'v' end
|
|
||||||
assert.are.same(vim.fn.visualmode(), 'v')
|
|
||||||
|
|
||||||
-- histget()
|
|
||||||
local orig_histget = vim.fn.histget
|
|
||||||
--- @diagnostic disable-next-line: duplicate-set-field
|
|
||||||
function vim.fn.histget(x, y) return [['<,'>]] end
|
|
||||||
|
|
||||||
-- Now run the test:
|
|
||||||
local range = Range.from_cmd_args(args) --[[@as u.Range]]
|
|
||||||
assert.are.same(range.start, a)
|
assert.are.same(range.start, a)
|
||||||
assert.are.same(range.stop, b)
|
assert.are.same(range.stop, b)
|
||||||
assert.are.same(range.mode, 'v')
|
assert.are.same(range.mode, 'v')
|
||||||
assert.are.same(range:text(), 'line')
|
|
||||||
|
|
||||||
-- Reset visualmode() and histget():
|
|
||||||
vim.fn.visualmode = orig_visualmode
|
|
||||||
vim.fn.histget = orig_histget
|
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -860,36 +667,4 @@ describe('Range', function()
|
|||||||
assert.are.same({ 3, 0 }, { details.end_row, details.end_col })
|
assert.are.same({ 3, 0 }, { details.end_row, details.end_col })
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('discerns range bounds from extmarks beyond the end of the buffer', function()
|
|
||||||
local Buffer = require 'u.buffer'
|
|
||||||
|
|
||||||
vim.cmd.vnew()
|
|
||||||
local left = Buffer.current()
|
|
||||||
left:set_tmp_options()
|
|
||||||
vim.cmd.vnew()
|
|
||||||
local right = Buffer.current()
|
|
||||||
right:set_tmp_options()
|
|
||||||
|
|
||||||
left:all():replace {
|
|
||||||
'one',
|
|
||||||
'two',
|
|
||||||
'three',
|
|
||||||
}
|
|
||||||
local left_all_ext = left:all():save_to_extmark()
|
|
||||||
|
|
||||||
right:all():replace {
|
|
||||||
'foo',
|
|
||||||
'bar',
|
|
||||||
}
|
|
||||||
|
|
||||||
vim.api.nvim_set_current_buf(right.bufnr)
|
|
||||||
vim.cmd [[normal! ggyG]]
|
|
||||||
vim.api.nvim_set_current_buf(left.bufnr)
|
|
||||||
vim.cmd [[normal! ggVGp]]
|
|
||||||
|
|
||||||
assert.are.same({ 'foo', 'bar' }, left_all_ext:range():lines())
|
|
||||||
vim.api.nvim_buf_delete(left.bufnr, { force = true })
|
|
||||||
vim.api.nvim_buf_delete(right.bufnr, { force = true })
|
|
||||||
end)
|
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -83,13 +83,13 @@ describe('Renderer', function()
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
--
|
--
|
||||||
-- get_tags_at
|
-- get_pos_infos
|
||||||
--
|
--
|
||||||
|
|
||||||
it('should return no extmarks for an empty buffer', function()
|
it('should return no extmarks for an empty buffer', function()
|
||||||
withbuf({}, function()
|
withbuf({}, function()
|
||||||
local r = R.Renderer.new(0)
|
local r = R.Renderer.new(0)
|
||||||
local pos_infos = r:get_tags_at { 0, 0 }
|
local pos_infos = r:get_pos_infos { 0, 0 }
|
||||||
assert.are.same(pos_infos, {})
|
assert.are.same(pos_infos, {})
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
@@ -102,28 +102,12 @@ describe('Renderer', function()
|
|||||||
R.h('text', { hl = 'HighlightGroup2' }, ' World'),
|
R.h('text', { hl = 'HighlightGroup2' }, ' World'),
|
||||||
}
|
}
|
||||||
|
|
||||||
local pos_infos = r:get_tags_at { 0, 2 }
|
local pos_infos = r:get_pos_infos { 0, 2 }
|
||||||
|
|
||||||
assert.are.same(#pos_infos, 1)
|
assert.are.same(#pos_infos, 1)
|
||||||
assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup1')
|
assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup1')
|
||||||
assert.are.same(pos_infos[1].extmark.start, { 0, 0 })
|
assert.are.same(pos_infos[1].extmark.start, { 0, 0 })
|
||||||
assert.are.same(pos_infos[1].extmark.stop, { 0, 5 })
|
assert.are.same(pos_infos[1].extmark.stop, { 0, 5 })
|
||||||
pos_infos = r:get_tags_at { 0, 4 }
|
|
||||||
assert.are.same(#pos_infos, 1)
|
|
||||||
assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup1')
|
|
||||||
assert.are.same(pos_infos[1].extmark.start, { 0, 0 })
|
|
||||||
assert.are.same(pos_infos[1].extmark.stop, { 0, 5 })
|
|
||||||
|
|
||||||
pos_infos = r:get_tags_at { 0, 5 }
|
|
||||||
assert.are.same(#pos_infos, 1)
|
|
||||||
assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup2')
|
|
||||||
assert.are.same(pos_infos[1].extmark.start, { 0, 5 })
|
|
||||||
assert.are.same(pos_infos[1].extmark.stop, { 0, 11 })
|
|
||||||
|
|
||||||
-- In insert mode, bounds are eagerly included:
|
|
||||||
pos_infos = r:get_tags_at({ 0, 5 }, 'i')
|
|
||||||
assert.are.same(#pos_infos, 2)
|
|
||||||
assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup1')
|
|
||||||
assert.are.same(pos_infos[2].tag.attributes.hl, 'HighlightGroup2')
|
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -141,252 +125,11 @@ describe('Renderer', function()
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
local pos_infos = r:get_tags_at { 0, 5 }
|
local pos_infos = r:get_pos_infos { 0, 5 }
|
||||||
|
|
||||||
assert.are.same(#pos_infos, 2)
|
assert.are.same(#pos_infos, 2)
|
||||||
assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup2')
|
assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup2')
|
||||||
assert.are.same(pos_infos[2].tag.attributes.hl, 'HighlightGroup1')
|
assert.are.same(pos_infos[2].tag.attributes.hl, 'HighlightGroup1')
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('repeated patch_lines calls should not change the buffer content', function()
|
|
||||||
local lines = {
|
|
||||||
[[{ {]],
|
|
||||||
[[ bounds = {]],
|
|
||||||
[[ start1 = { 1, 1 },]],
|
|
||||||
[[ stop1 = { 4, 1 }]],
|
|
||||||
[[ },]],
|
|
||||||
[[ end_right_gravity = true,]],
|
|
||||||
[[ id = 1,]],
|
|
||||||
[[ ns_id = 623,]],
|
|
||||||
[[ ns_name = "my.renderer:91",]],
|
|
||||||
[[ right_gravity = false]],
|
|
||||||
[[ } }]],
|
|
||||||
[[]],
|
|
||||||
}
|
|
||||||
withbuf(lines, function()
|
|
||||||
local Buffer = require 'u.buffer'
|
|
||||||
R.Renderer.patch_lines(0, nil, lines)
|
|
||||||
assert.are.same(Buffer.current():all():lines(), lines)
|
|
||||||
|
|
||||||
R.Renderer.patch_lines(0, lines, lines)
|
|
||||||
assert.are.same(Buffer.current():all():lines(), lines)
|
|
||||||
|
|
||||||
R.Renderer.patch_lines(0, lines, lines)
|
|
||||||
assert.are.same(Buffer.current():all():lines(), lines)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('should fire text-changed events', function()
|
|
||||||
withbuf({}, function()
|
|
||||||
local Buffer = require 'u.buffer'
|
|
||||||
local buf = Buffer.current()
|
|
||||||
local r = R.Renderer.new(0)
|
|
||||||
local captured_changed_text = ''
|
|
||||||
r:render {
|
|
||||||
R.h('text', {
|
|
||||||
on_change = function(txt) captured_changed_text = txt end,
|
|
||||||
}, {
|
|
||||||
'one\n',
|
|
||||||
'two\n',
|
|
||||||
'three\n',
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
vim.fn.setreg('"', 'bleh')
|
|
||||||
vim.cmd [[normal! ggVGp]]
|
|
||||||
-- For some reason, the autocmd does not fire in the busted environment.
|
|
||||||
-- We'll call the handler ourselves:
|
|
||||||
r:_on_text_changed()
|
|
||||||
|
|
||||||
assert.are.same(buf:all():text(), 'bleh')
|
|
||||||
assert.are.same(captured_changed_text, 'bleh')
|
|
||||||
|
|
||||||
vim.fn.setreg('"', '')
|
|
||||||
vim.cmd [[normal! ggdG]]
|
|
||||||
-- We'll call the handler ourselves:
|
|
||||||
r:_on_text_changed()
|
|
||||||
|
|
||||||
assert.are.same(buf:all():text(), '')
|
|
||||||
assert.are.same(captured_changed_text, '')
|
|
||||||
end)
|
|
||||||
|
|
||||||
withbuf({}, function()
|
|
||||||
local Buffer = require 'u.buffer'
|
|
||||||
local buf = Buffer.current()
|
|
||||||
local r = R.Renderer.new(0)
|
|
||||||
--- @type string?
|
|
||||||
local captured_changed_text = nil
|
|
||||||
r:render {
|
|
||||||
'prefix:',
|
|
||||||
R.h('text', {
|
|
||||||
on_change = function(txt) captured_changed_text = txt end,
|
|
||||||
}, {
|
|
||||||
'one',
|
|
||||||
}),
|
|
||||||
'suffix',
|
|
||||||
}
|
|
||||||
|
|
||||||
vim.fn.setreg('"', 'bleh')
|
|
||||||
vim.api.nvim_win_set_cursor(0, { 1, 9 })
|
|
||||||
vim.cmd [[normal! vhhd]]
|
|
||||||
-- For some reason, the autocmd does not fire in the busted environment.
|
|
||||||
-- We'll call the handler ourselves:
|
|
||||||
r:_on_text_changed()
|
|
||||||
|
|
||||||
assert.are.same(buf:all():text(), 'prefix:suffix')
|
|
||||||
assert.are.same(captured_changed_text, '')
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('should find tags by position', function()
|
|
||||||
withbuf({}, function()
|
|
||||||
local r = R.Renderer.new(0)
|
|
||||||
r:render {
|
|
||||||
'pre',
|
|
||||||
R.h('text', {
|
|
||||||
id = 'outer',
|
|
||||||
}, {
|
|
||||||
'inner-pre',
|
|
||||||
R.h('text', {
|
|
||||||
id = 'inner',
|
|
||||||
}, {
|
|
||||||
'inner-text',
|
|
||||||
}),
|
|
||||||
'inner-post',
|
|
||||||
}),
|
|
||||||
'post',
|
|
||||||
}
|
|
||||||
|
|
||||||
local tags = r:get_tags_at { 0, 11 }
|
|
||||||
assert.are.same(#tags, 1)
|
|
||||||
assert.are.same(tags[1].tag.attributes.id, 'outer')
|
|
||||||
|
|
||||||
tags = r:get_tags_at { 0, 12 }
|
|
||||||
assert.are.same(#tags, 2)
|
|
||||||
assert.are.same(tags[1].tag.attributes.id, 'inner')
|
|
||||||
assert.are.same(tags[2].tag.attributes.id, 'outer')
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('should find tags by id', function()
|
|
||||||
withbuf({}, function()
|
|
||||||
local r = R.Renderer.new(0)
|
|
||||||
r:render {
|
|
||||||
R.h('text', {
|
|
||||||
id = 'outer',
|
|
||||||
}, {
|
|
||||||
'inner-pre',
|
|
||||||
R.h('text', {
|
|
||||||
id = 'inner',
|
|
||||||
}, {
|
|
||||||
'inner-text',
|
|
||||||
}),
|
|
||||||
'inner-post',
|
|
||||||
}),
|
|
||||||
'post',
|
|
||||||
}
|
|
||||||
|
|
||||||
local bounds = r:get_tag_bounds 'outer'
|
|
||||||
assert.are.same(bounds, { start = { 0, 0 }, stop = { 0, 29 } })
|
|
||||||
|
|
||||||
bounds = r:get_tag_bounds 'inner'
|
|
||||||
assert.are.same(bounds, { start = { 0, 9 }, stop = { 0, 19 } })
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('should mount and rerender components', function()
|
|
||||||
withbuf({}, function()
|
|
||||||
--- @type any
|
|
||||||
local leaked_state = { app = {}, c1 = {}, c2 = {} }
|
|
||||||
|
|
||||||
--- @param ctx u.renderer.FnComponentContext<any, { phase: string, count: integer }>
|
|
||||||
local function Counter(ctx)
|
|
||||||
if ctx.phase == 'mount' then ctx.state = { phase = ctx.phase, count = 1 } end
|
|
||||||
local state = assert(ctx.state)
|
|
||||||
state.phase = ctx.phase
|
|
||||||
leaked_state[ctx.props.id].ctx = ctx
|
|
||||||
|
|
||||||
return {
|
|
||||||
{ 'Value: ', R.h.Number({}, tostring(state.count)) },
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
--- @param ctx u.renderer.FnComponentContext<any, {
|
|
||||||
--- toggle1: boolean,
|
|
||||||
--- show2: boolean
|
|
||||||
--- }>
|
|
||||||
function App(ctx)
|
|
||||||
if ctx.phase == 'mount' then ctx.state = { toggle1 = false, show2 = true } end
|
|
||||||
local state = assert(ctx.state)
|
|
||||||
leaked_state.app.ctx = ctx
|
|
||||||
|
|
||||||
return {
|
|
||||||
state.toggle1 and 'Toggle1' or R.h(Counter, { id = 'c1' }, {}),
|
|
||||||
'\n',
|
|
||||||
|
|
||||||
state.show2 and {
|
|
||||||
'\n',
|
|
||||||
R.h(Counter, { id = 'c2' }, {}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
local renderer = R.Renderer.new()
|
|
||||||
renderer:mount(R.h(App, {}, {}))
|
|
||||||
|
|
||||||
local Buffer = require 'u.buffer'
|
|
||||||
local buf = Buffer.current()
|
|
||||||
assert.are.same(buf:all():lines(), {
|
|
||||||
'Value: 1',
|
|
||||||
'',
|
|
||||||
'Value: 1',
|
|
||||||
})
|
|
||||||
assert.are.same(leaked_state.c1.ctx.state.phase, 'mount')
|
|
||||||
assert.are.same(leaked_state.c2.ctx.state.phase, 'mount')
|
|
||||||
leaked_state.app.ctx:update_immediate { toggle1 = true, show2 = true }
|
|
||||||
assert.are.same(buf:all():lines(), {
|
|
||||||
'Toggle1',
|
|
||||||
'',
|
|
||||||
'Value: 1',
|
|
||||||
})
|
|
||||||
assert.are.same(leaked_state.c1.ctx.state.phase, 'unmount')
|
|
||||||
assert.are.same(leaked_state.c2.ctx.state.phase, 'update')
|
|
||||||
|
|
||||||
leaked_state.app.ctx:update_immediate { toggle1 = true, show2 = false }
|
|
||||||
assert.are.same(buf:all():lines(), {
|
|
||||||
'Toggle1',
|
|
||||||
'',
|
|
||||||
})
|
|
||||||
assert.are.same(leaked_state.c1.ctx.state.phase, 'unmount')
|
|
||||||
assert.are.same(leaked_state.c2.ctx.state.phase, 'unmount')
|
|
||||||
|
|
||||||
leaked_state.app.ctx:update_immediate { toggle1 = false, show2 = true }
|
|
||||||
assert.are.same(buf:all():lines(), {
|
|
||||||
'Value: 1',
|
|
||||||
'',
|
|
||||||
'Value: 1',
|
|
||||||
})
|
|
||||||
assert.are.same(leaked_state.c1.ctx.state.phase, 'mount')
|
|
||||||
assert.are.same(leaked_state.c2.ctx.state.phase, 'mount')
|
|
||||||
|
|
||||||
leaked_state.c1.ctx:update_immediate { count = 2 }
|
|
||||||
assert.are.same(buf:all():lines(), {
|
|
||||||
'Value: 2',
|
|
||||||
'',
|
|
||||||
'Value: 1',
|
|
||||||
})
|
|
||||||
assert.are.same(leaked_state.c1.ctx.state.phase, 'update')
|
|
||||||
assert.are.same(leaked_state.c2.ctx.state.phase, 'update')
|
|
||||||
|
|
||||||
leaked_state.c2.ctx:update_immediate { count = 3 }
|
|
||||||
assert.are.same(buf:all():lines(), {
|
|
||||||
'Value: 2',
|
|
||||||
'',
|
|
||||||
'Value: 3',
|
|
||||||
})
|
|
||||||
assert.are.same(leaked_state.c1.ctx.state.phase, 'update')
|
|
||||||
assert.are.same(leaked_state.c2.ctx.state.phase, 'update')
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
syntax = "LuaJIT"
|
|
||||||
call_parentheses = "None"
|
call_parentheses = "None"
|
||||||
collapse_simple_statement = "Always"
|
collapse_simple_statement = "Always"
|
||||||
column_width = 100
|
column_width = 100
|
||||||
indent_type = "Spaces"
|
indent_type = "Spaces"
|
||||||
indent_width = 2
|
indent_width = 2
|
||||||
quote_style = "AutoPreferSingle"
|
quote_style = "AutoPreferSingle"
|
||||||
|
|
||||||
[sort_requires]
|
|
||||||
enabled = true
|
|
||||||
|
|||||||
Reference in New Issue
Block a user