Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9199a9bc3a |
12
.emmyrc.json
12
.emmyrc.json
@@ -1,12 +1,22 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/EmmyLuaLs/emmylua-analyzer-rust/refs/heads/main/crates/emmylua_code_analysis/resources/schema.json",
|
"$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": {
|
"runtime": {
|
||||||
"version": "LuaJIT"
|
"version": "LuaJIT"
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
|
"ignoreDir": [
|
||||||
|
".prefix"
|
||||||
|
],
|
||||||
"library": [
|
"library": [
|
||||||
"$VIMRUNTIME",
|
"$VIMRUNTIME",
|
||||||
"library/busted"
|
"library/busted",
|
||||||
|
"library/luv"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
69
.github/workflows/ci.yaml
vendored
69
.github/workflows/ci.yaml
vendored
@@ -1,29 +1,60 @@
|
|||||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||||
name: NeoVim tests
|
name: ci
|
||||||
on: [push]
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
tags: ["*"]
|
||||||
|
branches: ["*"]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
MISE_EXPERIMENTAL: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
code-quality:
|
ci:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
timeout-minutes: 10
|
||||||
XDG_CONFIG_HOME: ${{ github.workspace }}/.config/
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: cachix/install-nix-action@v31
|
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
submodules: true
|
||||||
|
|
||||||
- name: Populate Nix store
|
- name: Install apt dependencies
|
||||||
run:
|
run: |
|
||||||
nix-shell --pure --run 'true'
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libreadline-dev
|
||||||
|
|
||||||
- name: Type-check
|
- name: Setup environment
|
||||||
run:
|
run: |
|
||||||
nix-shell --pure --run 'make lint'
|
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
|
- name: Install mise
|
||||||
run:
|
run: |
|
||||||
nix-shell --pure --run 'make fmt-check'
|
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
|
- name: Install mise dependencies
|
||||||
run:
|
run: |
|
||||||
nix-shell --pure --run 'make test'
|
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
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
.prefix/
|
||||||
*.src.rock
|
*.src.rock
|
||||||
*.aider*
|
*.aider*
|
||||||
luacov.*.out
|
luacov.*.out
|
||||||
|
|||||||
8
.gitmodules
vendored
8
.gitmodules
vendored
@@ -2,3 +2,11 @@
|
|||||||
path = library/busted
|
path = library/busted
|
||||||
url = https://github.com/LuaCATS/busted
|
url = https://github.com/LuaCATS/busted
|
||||||
branch = main
|
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
|
||||||
|
|||||||
2
.luacheckrc
Normal file
2
.luacheckrc
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- :vim set ft=lua
|
||||||
|
globals = { "vim" }
|
||||||
16
.luarc.json
Normal file
16
.luarc.json
Normal 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
1
.styluaignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
library/
|
||||||
26
Makefile
26
Makefile
@@ -1,26 +0,0 @@
|
|||||||
export VIMRUNTIME := $(shell nvim -u NORC --headless +'echo $$VIMRUNTIME' +'quitall' 2>&1)
|
|
||||||
|
|
||||||
all: lint fmt-check test
|
|
||||||
|
|
||||||
lint:
|
|
||||||
@echo "## Typechecking"
|
|
||||||
@emmylua_check .
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -10,8 +10,12 @@
|
|||||||
-- change on the underlying filesystem.
|
-- change on the underlying filesystem.
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
--- @alias u.examples.FsDir { kind: 'dir', path: string, expanded: boolean, children: u.examples.FsNode[] }
|
--- @class u.examples.FsDir
|
||||||
--- @alias u.examples.FsFile { kind: 'file', path: string }
|
--- @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.FsNode u.examples.FsDir | u.examples.FsFile
|
||||||
--- @alias u.examples.ShowOpts { root_path?: string, width?: number, focus_path?: string }
|
--- @alias u.examples.ShowOpts { root_path?: string, width?: number, focus_path?: string }
|
||||||
|
|
||||||
@@ -112,12 +116,11 @@ function H.populate_dir_children(tree, path_to_node)
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param opts {
|
--- @class u.examples.RenderOpts
|
||||||
--- bufnr: number,
|
--- @field bufnr number
|
||||||
--- prev_winnr: number,
|
--- @field prev_winnr number
|
||||||
--- root_path: string,
|
--- @field root_path string
|
||||||
--- focus_path?: string
|
--- @field focus_path? string
|
||||||
--- }
|
|
||||||
---
|
---
|
||||||
--- @return { expand: fun(path: string), collapse: fun(path: string) }
|
--- @return { expand: fun(path: string), collapse: fun(path: string) }
|
||||||
local function _render_in_buffer(opts)
|
local function _render_in_buffer(opts)
|
||||||
@@ -406,11 +409,10 @@ end
|
|||||||
-- Public API functions:
|
-- Public API functions:
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
--- @type {
|
--- @class u.examples.CurrentInf
|
||||||
--- bufnr: number,
|
--- @field bufnr number
|
||||||
--- winnr: number,
|
--- @field winnr number
|
||||||
--- controller: { expand: fun(path: string), collapse: fun(path: string) }
|
--- @field controller table
|
||||||
--- } | nil
|
|
||||||
local current_inf = nil
|
local current_inf = nil
|
||||||
|
|
||||||
--- Show the filetree:
|
--- Show the filetree:
|
||||||
|
|||||||
@@ -22,12 +22,11 @@ vim.api.nvim_create_autocmd('VimResized', {
|
|||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
--- @alias u.examples.Notification {
|
--- @class u.examples.Notification
|
||||||
--- kind: number,
|
--- @field kind number
|
||||||
--- id: number,
|
--- @field id number
|
||||||
--- text: string,
|
--- @field text string
|
||||||
--- timer: uv.uv_timer_t
|
--- @field timer uv.uv_timer_t
|
||||||
--- }
|
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
|||||||
@@ -44,23 +44,21 @@ 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> {
|
--- @class u.examples.SelectController<T>
|
||||||
--- get_items: fun(): T[],
|
--- @field get_items fun(): T[]
|
||||||
--- set_items: fun(items: T[]),
|
--- @field set_items fun(items: T[])
|
||||||
--- set_filter_text: fun(filter_text: string),
|
--- @field set_filter_text fun(filter_text: string)
|
||||||
--- get_selected_indices: fun(): number[],
|
--- @field get_selected_indices fun(): number[]
|
||||||
--- get_selected_items: fun(): T[],
|
--- @field get_selected_items fun(): T[]
|
||||||
--- set_selected_indices: fun(indicies: number[], ephemeral?: boolean),
|
--- @field set_selected_indices fun(indices: number[], ephemeral?: boolean)
|
||||||
--- close: fun()
|
--- @field close fun()
|
||||||
--- }
|
--- @class u.examples.SelectOpts<T>
|
||||||
--- @alias u.examples.SelectOpts<T> {
|
--- @field items T[]
|
||||||
--- items: `T`[],
|
--- @field multi? boolean
|
||||||
--- multi?: boolean,
|
--- @field format_item? fun(item: T): u.renderer.Tree
|
||||||
--- format_item?: (fun(item: T): u.renderer.Tree),
|
--- @field on_finish? fun(items: T[], indices: number[])
|
||||||
--- on_finish?: (fun(items: T[], indicies: number[])),
|
--- @field on_selection_changed? fun(items: T[], indices: number[])
|
||||||
--- on_selection_changed?: fun(items: T[], indicies: number[]),
|
--- @field mappings? table<string, fun(select: u.examples.SelectController)>
|
||||||
--- mappings?: table<string, fun(select: u.examples.SelectController)>
|
|
||||||
--- }
|
|
||||||
|
|
||||||
--- @generic T
|
--- @generic T
|
||||||
--- @param opts u.examples.SelectOpts<T>
|
--- @param opts u.examples.SelectOpts<T>
|
||||||
|
|||||||
1
library/luv
Submodule
1
library/luv
Submodule
Submodule library/luv added at 3615eb12c9
@@ -3,8 +3,7 @@ local Range = require 'u.range'
|
|||||||
--- @type fun(range: u.Range): nil|(fun():any)
|
--- @type fun(range: u.Range): nil|(fun():any)
|
||||||
local __U__OpKeymapOpFunc_rhs = nil
|
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
|
|
||||||
--- @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)
|
||||||
|
|||||||
@@ -513,7 +513,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
|
||||||
|
|||||||
@@ -1,25 +1,13 @@
|
|||||||
---@diagnostic disable: redefined-local
|
|
||||||
function _G.URendererOpFuncSwallow() end
|
function _G.URendererOpFuncSwallow() end
|
||||||
|
|
||||||
--- Comma-separated list of regex patterns for debug logging. Examples:
|
local ENABLE_LOG = false
|
||||||
--- "render,extmark" or ".*" or "get_tags_at,_on_text_changed" or
|
|
||||||
--- "^_on_.*,reconcile$"
|
|
||||||
local DEBUG = vim.env.DEBUG or ''
|
|
||||||
|
|
||||||
--- @param name string
|
local function log(...)
|
||||||
local function log(name, ...)
|
if not ENABLE_LOG then return end
|
||||||
if DEBUG == '' then return end
|
|
||||||
|
|
||||||
for pattern in DEBUG:gmatch '[^,]+' do
|
local f = assert(io.open(vim.fs.joinpath(vim.fn.stdpath 'log', 'u.renderer.log'), 'a+'))
|
||||||
if name:match(vim.trim(pattern)) then
|
f:write(os.date() .. '\t' .. vim.inspect { ... } .. '\n')
|
||||||
local f = assert(
|
f:close()
|
||||||
io.open(vim.fs.joinpath((vim.fn.stdpath 'log') --[[@as string]], 'u.renderer.log'), 'a+')
|
|
||||||
)
|
|
||||||
f:write(os.date() .. '\t' .. vim.inspect { name, ... } .. '\n')
|
|
||||||
f:close()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
@@ -27,59 +15,19 @@ local H = {}
|
|||||||
|
|
||||||
--- @alias u.renderer.TagEventHandler fun(tag: u.renderer.Tag, mode: string, lhs: string): string
|
--- @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), key?: string|number }
|
--- @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 }
|
||||||
|
|
||||||
--- @generic TProps
|
|
||||||
--- @generic TState
|
|
||||||
--- @class u.renderer.FnComponentContext<TProps, TState>
|
|
||||||
--- @field phase 'mount'|'update'|'unmount'
|
|
||||||
--- @field props TProps
|
|
||||||
--- @field state? TState
|
|
||||||
--- @field children u.renderer.Tree
|
|
||||||
--- @field private on_change? fun(): any
|
|
||||||
--- @field private prev_rendered_children? u.renderer.Tree
|
|
||||||
local FnComponentContext = {}
|
|
||||||
M.FnComponentContext = FnComponentContext
|
|
||||||
FnComponentContext.__index = FnComponentContext
|
|
||||||
|
|
||||||
--- @param props TProps
|
|
||||||
--- @param state? TState
|
|
||||||
--- @param children u.renderer.Tree
|
|
||||||
function FnComponentContext.new(props, state, children)
|
|
||||||
return setmetatable({
|
|
||||||
phase = 'mount',
|
|
||||||
props = props,
|
|
||||||
state = state,
|
|
||||||
children = children,
|
|
||||||
}, FnComponentContext)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- @param new_state TState
|
|
||||||
function FnComponentContext:update(new_state)
|
|
||||||
self.state = new_state
|
|
||||||
if self.on_change then vim.schedule(function() self.on_change() end) end
|
|
||||||
end
|
|
||||||
|
|
||||||
--- @private
|
|
||||||
--- @param new_state TState
|
|
||||||
function FnComponentContext:update_immediate(new_state)
|
|
||||||
self.state = new_state
|
|
||||||
if self.on_change then self.on_change() end
|
|
||||||
end
|
|
||||||
|
|
||||||
--- @alias u.renderer.FnComponent<TProps, TState> fun(ctx: u.renderer.FnComponentContext<TProps, TState>): u.renderer.Tree
|
|
||||||
|
|
||||||
--- @class u.renderer.Tag
|
--- @class u.renderer.Tag
|
||||||
--- @field kind 'tag'
|
--- @field kind 'tag'
|
||||||
--- @field name string | u.renderer.FnComponent<any, any>
|
--- @field name string
|
||||||
--- @field attributes u.renderer.TagAttributes
|
--- @field attributes u.renderer.TagAttributes
|
||||||
--- @field children u.renderer.Tree
|
--- @field children u.renderer.Tree
|
||||||
--- @field private ctx? u.renderer.FnComponentContext
|
|
||||||
|
|
||||||
--- @alias u.renderer.Node nil | boolean | string | u.renderer.Tag
|
--- @alias u.renderer.Node nil | boolean | string | u.renderer.Tag
|
||||||
--- @alias u.renderer.Tree u.renderer.Node | u.renderer.Node[]
|
--- @alias u.renderer.Tree u.renderer.Node | u.renderer.Node[]
|
||||||
|
|
||||||
-- luacheck: ignore
|
-- luacheck: ignore
|
||||||
|
--- @type table<string, fun(attributes: u.renderer.TagAttributes, children: u.renderer.Tree): u.renderer.Tag> & fun(name: string, attributes: u.renderer.TagAttributes, children: u.renderer.Tree): u.renderer.Tag>
|
||||||
M.h = setmetatable({}, {
|
M.h = setmetatable({}, {
|
||||||
__call = function(_, name, attributes, children)
|
__call = function(_, name, attributes, children)
|
||||||
return {
|
return {
|
||||||
@@ -94,77 +42,39 @@ M.h = setmetatable({}, {
|
|||||||
return M.h('text', vim.tbl_deep_extend('force', { hl = name }, attributes), children)
|
return M.h('text', vim.tbl_deep_extend('force', { hl = name }, attributes), children)
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
}) --[[@as table<string, fun(attributes: u.renderer.TagAttributes, children: u.renderer.Tree): u.renderer.Tag> & fun(name: string, attributes: u.renderer.TagAttributes, children: u.renderer.Tree): u.renderer.Tag>]]
|
})
|
||||||
|
|
||||||
-- Renderer {{{
|
-- Renderer {{{
|
||||||
--- @class u.renderer.RendererExtmark
|
--- @class u.renderer.RendererExtmark
|
||||||
--- @field id? integer
|
--- @field id? number
|
||||||
--- @field start [integer, integer]
|
--- @field start [number, number]
|
||||||
--- @field stop [integer, integer]
|
--- @field stop [number, number]
|
||||||
--- @field opts vim.api.keyset.set_extmark
|
--- @field opts vim.api.keyset.set_extmark
|
||||||
--- @field tag u.renderer.Tag
|
--- @field tag u.renderer.Tag
|
||||||
|
|
||||||
--- @class u.renderer.Renderer
|
--- @class u.renderer.Renderer
|
||||||
--- @field bufnr integer
|
--- @field bufnr number
|
||||||
--- @field ns integer
|
--- @field ns number
|
||||||
--- @field changedtick integer
|
--- @field changedtick number
|
||||||
--- @field text_content { old: { lines: string[], extmarks: u.renderer.RendererExtmark[] }, curr: { lines: string[], extmarks: u.renderer.RendererExtmark[] } }
|
--- @field old { lines: string[], extmarks: u.renderer.RendererExtmark[] }
|
||||||
--- @field component_tree { old: u.renderer.Tree }
|
--- @field curr { lines: string[], extmarks: u.renderer.RendererExtmark[] }
|
||||||
local Renderer = {}
|
local Renderer = {}
|
||||||
Renderer.__index = Renderer
|
Renderer.__index = Renderer
|
||||||
M.Renderer = Renderer
|
M.Renderer = Renderer
|
||||||
|
|
||||||
--- @private
|
--- @private
|
||||||
--- @param tree u.renderer.Tree
|
--- @param x any
|
||||||
--- @param visitors {
|
--- @return boolean
|
||||||
--- nil_?: (fun(): any),
|
function Renderer.is_tag(x) return type(x) == 'table' and x.kind == 'tag' end
|
||||||
--- boolean?: (fun(b: boolean): any),
|
|
||||||
--- string?: (fun(s: string): any),
|
|
||||||
--- array?: (fun(tags: u.renderer.Node[]): any),
|
|
||||||
--- tag?: (fun(tag: u.renderer.Tag): any),
|
|
||||||
--- component?: (fun(component: u.renderer.FnComponent, tag: u.renderer.Tag): any),
|
|
||||||
--- unknown?: fun(tag: any): any
|
|
||||||
--- }
|
|
||||||
function Renderer.tree_match(tree, visitors) -- {{{
|
|
||||||
local function is_tag(x) return type(x) == 'table' and x.kind == 'tag' end
|
|
||||||
local function is_tag_arr(x) return type(x) == 'table' and not is_tag(x) end
|
|
||||||
|
|
||||||
if tree == nil then
|
|
||||||
return visitors.nil_ and visitors.nil_() or nil
|
|
||||||
elseif type(tree) == 'boolean' then
|
|
||||||
return visitors.boolean and visitors.boolean(tree) or nil
|
|
||||||
elseif type(tree) == 'string' then
|
|
||||||
return visitors.string and visitors.string(tree) or nil
|
|
||||||
elseif is_tag_arr(tree) then
|
|
||||||
return visitors.array and visitors.array(tree --[[@as any]]) or nil
|
|
||||||
elseif is_tag(tree) then
|
|
||||||
local tag = tree --[[@as u.renderer.Tag]]
|
|
||||||
if type(tag.name) == 'function' then
|
|
||||||
return visitors.component and visitors.component(tag.name --[[@as function]], tag) or nil
|
|
||||||
else
|
|
||||||
return visitors.tag and visitors.tag(tree --[[@as any]]) or nil
|
|
||||||
end
|
|
||||||
else
|
|
||||||
return visitors.unknown and visitors.unknown(tree) or error 'unknown value: not a tag'
|
|
||||||
end
|
|
||||||
end -- }}}
|
|
||||||
|
|
||||||
--- @private
|
--- @private
|
||||||
--- @param tree u.renderer.Tree
|
--- @param x any
|
||||||
--- @return 'nil' | 'boolean' | 'string' | 'array' | 'tag' | u.renderer.FnComponent | 'unknown'
|
--- @return boolean
|
||||||
function Renderer.tree_kind(tree) -- {{{
|
function Renderer.is_tag_arr(x)
|
||||||
return Renderer.tree_match(tree, {
|
if type(x) ~= 'table' then return false end
|
||||||
nil_ = function() return 'nil' end,
|
return #x == 0 or not Renderer.is_tag(x)
|
||||||
boolean = function() return 'boolean' end,
|
end
|
||||||
string = function() return 'string' end,
|
--- @param bufnr number|nil
|
||||||
array = function() return 'array' end,
|
|
||||||
tag = function() return 'tag' end,
|
|
||||||
component = function(c) return c end,
|
|
||||||
unknown = function() return 'unknown' end,
|
|
||||||
}) --[[@as any]]
|
|
||||||
end -- }}}
|
|
||||||
|
|
||||||
--- @param bufnr integer|nil
|
|
||||||
function Renderer.new(bufnr) -- {{{
|
function Renderer.new(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
|
||||||
|
|
||||||
@@ -176,15 +86,8 @@ function Renderer.new(bufnr) -- {{{
|
|||||||
bufnr = bufnr,
|
bufnr = bufnr,
|
||||||
ns = vim.b[bufnr]._renderer_ns,
|
ns = vim.b[bufnr]._renderer_ns,
|
||||||
changedtick = 0,
|
changedtick = 0,
|
||||||
text_content = {
|
old = { lines = {}, extmarks = {} },
|
||||||
old = { lines = {}, extmarks = {} },
|
curr = { lines = {}, extmarks = {} },
|
||||||
curr = { lines = {}, extmarks = {} },
|
|
||||||
},
|
|
||||||
component_tree = {
|
|
||||||
old = nil,
|
|
||||||
curr = nil,
|
|
||||||
ctx_by_node = {},
|
|
||||||
},
|
|
||||||
}, Renderer)
|
}, Renderer)
|
||||||
|
|
||||||
vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI', 'TextChangedP' }, {
|
vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI', 'TextChangedP' }, {
|
||||||
@@ -195,10 +98,9 @@ function Renderer.new(bufnr) -- {{{
|
|||||||
return self
|
return self
|
||||||
end -- }}}
|
end -- }}}
|
||||||
|
|
||||||
--- @param opts {
|
--- @class u.renderer.MarkupOpts
|
||||||
--- tree: u.renderer.Tree,
|
--- @field tree u.renderer.Tree
|
||||||
--- on_tag?: fun(tag: u.renderer.Tag, start0: [number, number], stop0: [number, number]): any
|
--- @field on_tag? fun(tag: u.renderer.Tag, start0: [number, number], stop0: [number, number]): any
|
||||||
--- }
|
|
||||||
function Renderer.markup_to_lines(opts) -- {{{
|
function Renderer.markup_to_lines(opts) -- {{{
|
||||||
--- @type string[]
|
--- @type string[]
|
||||||
local lines = {}
|
local lines = {}
|
||||||
@@ -216,44 +118,51 @@ function Renderer.markup_to_lines(opts) -- {{{
|
|||||||
curr_col1 = 1
|
curr_col1 = 1
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param node u.renderer.Tree
|
--- @param node u.renderer.Node
|
||||||
local function visit(node) -- {{{
|
local function visit(node) -- {{{
|
||||||
Renderer.tree_match(node, {
|
if node == nil or type(node) == 'boolean' then return end
|
||||||
string = function(s_node)
|
|
||||||
local node_lines = vim.split(s_node, '\n')
|
if type(node) == 'string' then
|
||||||
for lnum, s in ipairs(node_lines) do
|
local node_lines = vim.split(node, '\n')
|
||||||
if lnum > 1 then put_line() end
|
for lnum, s in ipairs(node_lines) do
|
||||||
put(s)
|
if lnum > 1 then put_line() end
|
||||||
end
|
put(s)
|
||||||
end,
|
end
|
||||||
array = function(ts)
|
elseif Renderer.is_tag(node) then
|
||||||
for _, child in ipairs(ts) do
|
local start0 = { curr_line1 - 1, curr_col1 - 1 }
|
||||||
|
|
||||||
|
-- visit the children:
|
||||||
|
if Renderer.is_tag_arr(node.children) then
|
||||||
|
for _, child in
|
||||||
|
ipairs(node.children --[[@as u.renderer.Node[] ]])
|
||||||
|
do
|
||||||
|
-- newlines are not controlled by array entries, do NOT output a line here:
|
||||||
visit(child)
|
visit(child)
|
||||||
end
|
end
|
||||||
end,
|
else -- luacheck: ignore
|
||||||
tag = function(t)
|
visit(node.children)
|
||||||
assert(type(t.name) ~= 'function', 'markup_to_lines does not render components')
|
end
|
||||||
|
|
||||||
local start0 = { curr_line1 - 1, curr_col1 - 1 }
|
local stop0 = { curr_line1 - 1, curr_col1 - 1 }
|
||||||
visit(t.children)
|
if opts.on_tag then opts.on_tag(node, start0, stop0) end
|
||||||
local stop0 = { curr_line1 - 1, curr_col1 - 1 }
|
elseif Renderer.is_tag_arr(node) then
|
||||||
|
for _, child in ipairs(node) do
|
||||||
if opts.on_tag then opts.on_tag(t, start0, stop0) end
|
-- newlines are not controlled by array entries, do NOT output a line here:
|
||||||
end,
|
visit(child)
|
||||||
})
|
end
|
||||||
|
end
|
||||||
end -- }}}
|
end -- }}}
|
||||||
visit(opts.tree)
|
visit(opts.tree)
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
end -- }}}
|
end -- }}}
|
||||||
|
|
||||||
--- @param opts {
|
--- @class u.renderer.StringOpts
|
||||||
--- tree: u.renderer.Tree,
|
--- @field tree u.renderer.Tree
|
||||||
--- format_tag?: fun(tag: u.renderer.Tag): string
|
--- @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
|
function Renderer.markup_to_string(opts) return table.concat(Renderer.markup_to_lines(opts), '\n') end
|
||||||
|
|
||||||
--- @param bufnr integer
|
--- @param bufnr number
|
||||||
--- @param old_lines string[] | nil
|
--- @param old_lines string[] | nil
|
||||||
--- @param new_lines string[]
|
--- @param new_lines string[]
|
||||||
function Renderer.patch_lines(bufnr, old_lines, new_lines) -- {{{
|
function Renderer.patch_lines(bufnr, old_lines, new_lines) -- {{{
|
||||||
@@ -279,10 +188,8 @@ function Renderer.patch_lines(bufnr, old_lines, new_lines) -- {{{
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Morph the text to the desired state:
|
-- Morph the text to the desired state:
|
||||||
local line_changes = H.levenshtein {
|
local line_changes =
|
||||||
from = old_lines or vim.api.nvim_buf_get_lines(bufnr, 0, -1, false),
|
H.levenshtein(old_lines or vim.api.nvim_buf_get_lines(bufnr, 0, -1, false), new_lines)
|
||||||
to = new_lines,
|
|
||||||
}
|
|
||||||
for _, line_change in ipairs(line_changes) do
|
for _, line_change in ipairs(line_changes) do
|
||||||
local lnum0 = line_change.index - 1
|
local lnum0 = line_change.index - 1
|
||||||
|
|
||||||
@@ -291,7 +198,7 @@ function Renderer.patch_lines(bufnr, old_lines, new_lines) -- {{{
|
|||||||
elseif line_change.kind == 'change' then
|
elseif line_change.kind == 'change' then
|
||||||
-- Compute inter-line diff, and apply:
|
-- Compute inter-line diff, and apply:
|
||||||
local col_changes =
|
local col_changes =
|
||||||
H.levenshtein { from = vim.split(line_change.from, ''), to = vim.split(line_change.to, '') }
|
H.levenshtein(vim.split(line_change.from, ''), vim.split(line_change.to, ''))
|
||||||
|
|
||||||
for _, col_change in ipairs(col_changes) do
|
for _, col_change in ipairs(col_changes) do
|
||||||
local cnum0 = col_change.index - 1
|
local cnum0 = col_change.index - 1
|
||||||
@@ -317,8 +224,7 @@ end -- }}}
|
|||||||
function Renderer:render(tree) -- {{{
|
function Renderer:render(tree) -- {{{
|
||||||
local changedtick = vim.b[self.bufnr].changedtick
|
local changedtick = vim.b[self.bufnr].changedtick
|
||||||
if changedtick ~= self.changedtick then
|
if changedtick ~= self.changedtick then
|
||||||
self.text_content.curr =
|
self.curr = { lines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) }
|
||||||
{ extmarks = {}, lines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) }
|
|
||||||
self.changedtick = changedtick
|
self.changedtick = changedtick
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -375,8 +281,8 @@ function Renderer:render(tree) -- {{{
|
|||||||
end, -- }}}
|
end, -- }}}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.text_content.old = self.text_content.curr
|
self.old = self.curr
|
||||||
self.text_content.curr = { lines = lines, extmarks = extmarks }
|
self.curr = { lines = lines, extmarks = extmarks }
|
||||||
self:_reconcile()
|
self:_reconcile()
|
||||||
vim.cmd.doautocmd { args = { 'User', 'Renderer:' .. tostring(self.bufnr) .. ':render' } }
|
vim.cmd.doautocmd { args = { 'User', 'Renderer:' .. tostring(self.bufnr) .. ':render' } }
|
||||||
end -- }}}
|
end -- }}}
|
||||||
@@ -386,7 +292,7 @@ function Renderer:_reconcile() -- {{{
|
|||||||
--
|
--
|
||||||
-- Step 1: morph the text to the desired state:
|
-- Step 1: morph the text to the desired state:
|
||||||
--
|
--
|
||||||
Renderer.patch_lines(self.bufnr, self.text_content.old.lines, self.text_content.curr.lines)
|
Renderer.patch_lines(self.bufnr, self.old.lines, self.curr.lines)
|
||||||
self.changedtick = vim.b[self.bufnr].changedtick
|
self.changedtick = vim.b[self.bufnr].changedtick
|
||||||
|
|
||||||
--
|
--
|
||||||
@@ -401,7 +307,7 @@ function Renderer:_reconcile() -- {{{
|
|||||||
vim.api.nvim_buf_clear_namespace(self.bufnr, self.ns, 0, -1)
|
vim.api.nvim_buf_clear_namespace(self.bufnr, self.ns, 0, -1)
|
||||||
|
|
||||||
-- Set current extmarks:
|
-- Set current extmarks:
|
||||||
for _, extmark in ipairs(self.text_content.curr.extmarks) do
|
for _, extmark in ipairs(self.curr.extmarks) do
|
||||||
extmark.id = vim.api.nvim_buf_set_extmark(
|
extmark.id = vim.api.nvim_buf_set_extmark(
|
||||||
self.bufnr,
|
self.bufnr,
|
||||||
self.ns,
|
self.ns,
|
||||||
@@ -422,7 +328,7 @@ function Renderer:_reconcile() -- {{{
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
self.text_content.old = self.text_content.curr
|
self.old = self.curr
|
||||||
end -- }}}
|
end -- }}}
|
||||||
|
|
||||||
--- @private
|
--- @private
|
||||||
@@ -432,9 +338,9 @@ function Renderer:_expr_map_callback(mode, lhs) -- {{{
|
|||||||
-- find the tag with the smallest intersection that contains the cursor:
|
-- find the tag with the smallest intersection that contains the cursor:
|
||||||
local pos0 = vim.api.nvim_win_get_cursor(0)
|
local pos0 = vim.api.nvim_win_get_cursor(0)
|
||||||
pos0[1] = pos0[1] - 1 -- make it actually 0-based
|
pos0[1] = pos0[1] - 1 -- make it actually 0-based
|
||||||
log('_expr_map_callback', 'pos0:', pos0)
|
log('_expr_map_callback: pos0:', pos0)
|
||||||
local pos_infos = self:get_tags_at(pos0)
|
local pos_infos = self:get_tags_at(pos0)
|
||||||
log('_expr_map_callback', 'pos_infos:', pos_infos)
|
log('_expr_map_callback: pos_infos:', pos_infos)
|
||||||
|
|
||||||
if #pos_infos == 0 then return lhs end
|
if #pos_infos == 0 then return lhs end
|
||||||
|
|
||||||
@@ -484,7 +390,7 @@ function Renderer:_on_text_changed() -- {{{
|
|||||||
--- @type integer, integer, vim.api.keyset.extmark_details
|
--- @type integer, integer, vim.api.keyset.extmark_details
|
||||||
local start_row0, start_col0, details = unpack(extmark)
|
local start_row0, start_col0, details = unpack(extmark)
|
||||||
local end_row0, end_col0 = details.end_row, details.end_col
|
local end_row0, end_col0 = details.end_row, details.end_col
|
||||||
log('_on_text_changed', 'fetched current extmark for pos_info', {
|
log('_on_text_changed: fetched current extmark for pos_info', {
|
||||||
pos_info = pos_info,
|
pos_info = pos_info,
|
||||||
curr_extmark = {
|
curr_extmark = {
|
||||||
start_row0 = start_row0,
|
start_row0 = start_row0,
|
||||||
@@ -513,7 +419,7 @@ function Renderer:_on_text_changed() -- {{{
|
|||||||
or ''
|
or ''
|
||||||
end_col0 = last_line:len()
|
end_col0 = last_line:len()
|
||||||
end
|
end
|
||||||
log('_on_text_changed', 'after position correction', {
|
log('_on_text_changed: after position correction', {
|
||||||
curr_extmark = {
|
curr_extmark = {
|
||||||
start_row0 = start_row0,
|
start_row0 = start_row0,
|
||||||
start_col0 = start_col0,
|
start_col0 = start_col0,
|
||||||
@@ -531,7 +437,7 @@ function Renderer:_on_text_changed() -- {{{
|
|||||||
local pos2 = { self.bufnr, end_row0 + 1, end_col0 }
|
local pos2 = { self.bufnr, end_row0 + 1, end_col0 }
|
||||||
local ok, lines = pcall(vim.fn.getregion, pos1, pos2, { type = 'v' })
|
local ok, lines = pcall(vim.fn.getregion, pos1, pos2, { type = 'v' })
|
||||||
if not ok then
|
if not ok then
|
||||||
log('_on_text_changed', 'getregion: invalid-pos ', {
|
log('_on_text_changed: getregion: invalid-pos ', {
|
||||||
{ pos1, pos2 },
|
{ pos1, pos2 },
|
||||||
})
|
})
|
||||||
vim.api.nvim_echo({
|
vim.api.nvim_echo({
|
||||||
@@ -584,7 +490,7 @@ function Renderer:_debug() -- {{{
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
computed = {
|
computed = {
|
||||||
extmarks = self.text_content.curr.extmarks,
|
extmarks = self.curr.extmarks,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
vim.api.nvim_buf_set_lines(info_bufnr, 0, -1, true, vim.split(vim.inspect(info), '\n'))
|
vim.api.nvim_buf_set_lines(info_bufnr, 0, -1, true, vim.split(vim.inspect(info), '\n'))
|
||||||
@@ -626,7 +532,7 @@ end -- }}}
|
|||||||
--- (outermost).
|
--- (outermost).
|
||||||
---
|
---
|
||||||
--- @private (private for now)
|
--- @private (private for now)
|
||||||
--- @param pos0 [integer, integer]
|
--- @param pos0 [number, number]
|
||||||
--- @param mode string?
|
--- @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) -- {{{
|
function Renderer:get_tags_at(pos0, mode) -- {{{
|
||||||
@@ -642,8 +548,7 @@ function Renderer:get_tags_at(pos0, mode) -- {{{
|
|||||||
{ details = true, overlap = true }
|
{ details = true, overlap = true }
|
||||||
)
|
)
|
||||||
log(
|
log(
|
||||||
'get_tags_at',
|
'get_tags_at: context:',
|
||||||
'context:',
|
|
||||||
{ pos0 = pos0, mode = mode, raw_overlapping_extmarks = raw_overlapping_extmarks }
|
{ pos0 = pos0, mode = mode, raw_overlapping_extmarks = raw_overlapping_extmarks }
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -653,65 +558,56 @@ function Renderer:get_tags_at(pos0, mode) -- {{{
|
|||||||
--- @type u.renderer.RendererExtmark[]
|
--- @type u.renderer.RendererExtmark[]
|
||||||
local mapped_extmarks = vim
|
local mapped_extmarks = vim
|
||||||
.iter(raw_overlapping_extmarks)
|
.iter(raw_overlapping_extmarks)
|
||||||
:map(
|
:map(function(ext)
|
||||||
--- @return u.renderer.RendererExtmark
|
--- @type number, number, number, { end_row?: number, end_col?: number }|nil
|
||||||
function(ext)
|
local id, line0, col0, details = unpack(ext)
|
||||||
--- @type integer, integer, integer, { end_row?: number, end_col?: number }|nil
|
local start = { line0, col0 }
|
||||||
local id, line0, col0, details = unpack(ext)
|
local stop = { line0, col0 }
|
||||||
local start = { line0, col0 }
|
if details and details.end_row ~= nil and details.end_col ~= nil then
|
||||||
local stop = { line0, col0 }
|
stop = { details.end_row, details.end_col }
|
||||||
if details and details.end_row ~= nil and details.end_col ~= nil then
|
|
||||||
stop = {
|
|
||||||
details.end_row --[[@as integer]],
|
|
||||||
details.end_col --[[@as integer]],
|
|
||||||
}
|
|
||||||
end
|
|
||||||
return { id = id, start = start, stop = stop, opts = details }
|
|
||||||
end
|
end
|
||||||
)
|
return { id = id, start = start, stop = stop, opts = details }
|
||||||
|
end)
|
||||||
:totable()
|
:totable()
|
||||||
|
|
||||||
local intersecting_extmarks = vim
|
local intersecting_extmarks = vim
|
||||||
.iter(mapped_extmarks)
|
.iter(mapped_extmarks)
|
||||||
:filter(
|
:filter(function(ext)
|
||||||
--- @param ext u.renderer.RendererExtmark
|
if ext.stop[1] ~= nil and ext.stop[2] ~= nil then
|
||||||
function(ext)
|
-- If we've "ciw" and "collapsed" an extmark onto the cursor,
|
||||||
if ext.stop[1] ~= nil and ext.stop[2] ~= nil then
|
-- the cursor pos will equal the exmark's start AND end. In this
|
||||||
-- If we've "ciw" and "collapsed" an extmark onto the cursor,
|
-- case, we want to include the extmark.
|
||||||
-- the cursor pos will equal the exmark's start AND end. In this
|
if
|
||||||
-- case, we want to include the extmark.
|
cursor_line0 == ext.start[1]
|
||||||
if
|
and cursor_col0 == ext.start[2]
|
||||||
cursor_line0 == ext.start[1]
|
and cursor_line0 == ext.stop[1]
|
||||||
and cursor_col0 == ext.start[2]
|
and cursor_col0 == ext.stop[2]
|
||||||
and cursor_line0 == ext.stop[1]
|
then
|
||||||
and cursor_col0 == ext.stop[2]
|
|
||||||
then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
return
|
|
||||||
-- START: line check
|
|
||||||
cursor_line0 >= ext.start[1]
|
|
||||||
-- START: column check
|
|
||||||
and (cursor_line0 ~= ext.start[1] or cursor_col0 >= ext.start[2])
|
|
||||||
-- STOP: line check
|
|
||||||
and cursor_line0 <= ext.stop[1]
|
|
||||||
-- STOP: column check
|
|
||||||
and (
|
|
||||||
cursor_line0 ~= ext.stop[1]
|
|
||||||
or (
|
|
||||||
mode == 'i'
|
|
||||||
-- In insert mode, the cursor is "thin", so <= to compensate:
|
|
||||||
and cursor_col0 <= ext.stop[2]
|
|
||||||
-- In normal mode, the cursor is "wide", so < to compensate:
|
|
||||||
or cursor_col0 < ext.stop[2]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else
|
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
return
|
||||||
|
-- START: line check
|
||||||
|
cursor_line0 >= ext.start[1]
|
||||||
|
-- START: column check
|
||||||
|
and (cursor_line0 ~= ext.start[1] or cursor_col0 >= ext.start[2])
|
||||||
|
-- STOP: line check
|
||||||
|
and cursor_line0 <= ext.stop[1]
|
||||||
|
-- STOP: column check
|
||||||
|
and (
|
||||||
|
cursor_line0 ~= ext.stop[1]
|
||||||
|
or (
|
||||||
|
mode == 'i'
|
||||||
|
-- In insert mode, the cursor is "thin", so <= to compensate:
|
||||||
|
and cursor_col0 <= ext.stop[2]
|
||||||
|
-- In normal mode, the cursor is "wide", so < to compensate:
|
||||||
|
or cursor_col0 < ext.stop[2]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
return true
|
||||||
end
|
end
|
||||||
)
|
end)
|
||||||
:totable()
|
:totable()
|
||||||
|
|
||||||
-- Sort the tags into smallest (inner) to largest (outer):
|
-- Sort the tags into smallest (inner) to largest (outer):
|
||||||
@@ -737,20 +633,17 @@ function Renderer:get_tags_at(pos0, mode) -- {{{
|
|||||||
)
|
)
|
||||||
|
|
||||||
-- When we set the extmarks in the step above, we captured the IDs of the
|
-- When we set the extmarks in the step above, we captured the IDs of the
|
||||||
-- created extmarks in self.text_content.curr.extmarks, which also has which tag each
|
-- 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
|
-- extmark is associated with. Cross-reference with that list to get a list
|
||||||
-- of tags that we need to fire events for:
|
-- 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
|
local matching_tags = vim
|
||||||
.iter(intersecting_extmarks)
|
.iter(intersecting_extmarks)
|
||||||
:map(
|
:map(function(ext)
|
||||||
--- @param ext u.renderer.RendererExtmark
|
for _, extmark_cache in ipairs(self.curr.extmarks) do
|
||||||
function(ext)
|
if extmark_cache.id == ext.id then return { extmark = ext, tag = extmark_cache.tag } end
|
||||||
for _, extmark_cache in ipairs(self.text_content.curr.extmarks) do
|
|
||||||
if extmark_cache.id == ext.id then return { extmark = ext, tag = extmark_cache.tag } end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
)
|
end)
|
||||||
:totable()
|
:totable()
|
||||||
|
|
||||||
return matching_tags
|
return matching_tags
|
||||||
@@ -760,7 +653,7 @@ end -- }}}
|
|||||||
--- @param tag_or_id string | u.renderer.Tag
|
--- @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) -- {{{
|
function Renderer:get_tag_bounds(tag_or_id) -- {{{
|
||||||
for _, x in ipairs(self.text_content.curr.extmarks) do
|
for _, x in ipairs(self.curr.extmarks) do
|
||||||
local pos = { start = x.start, stop = x.stop }
|
local pos = { start = x.start, stop = x.stop }
|
||||||
local does_tag_match = type(tag_or_id) == 'string' and x.tag.attributes.id == tag_or_id
|
local does_tag_match = type(tag_or_id) == 'string' and x.tag.attributes.id == tag_or_id
|
||||||
or x.tag == tag_or_id
|
or x.tag == tag_or_id
|
||||||
@@ -768,189 +661,6 @@ function Renderer:get_tag_bounds(tag_or_id) -- {{{
|
|||||||
end
|
end
|
||||||
end -- }}}
|
end -- }}}
|
||||||
|
|
||||||
--- @param tree u.renderer.Tree
|
|
||||||
function Renderer:mount(tree) -- {{{
|
|
||||||
local function rerender()
|
|
||||||
local H2 = {}
|
|
||||||
|
|
||||||
--- @param tree u.renderer.Tree
|
|
||||||
function H2.unmount(tree)
|
|
||||||
log('Render.mount.umount', { tree = tree })
|
|
||||||
--- @param tree u.renderer.Tree
|
|
||||||
local function visit(tree)
|
|
||||||
Renderer.tree_match(tree, {
|
|
||||||
array = function(tags)
|
|
||||||
log('Render.mount.umount.array', { tags = tags })
|
|
||||||
for _, tag in ipairs(tags) do
|
|
||||||
visit(tag)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
tag = function(tag)
|
|
||||||
log('Render.mount.umount.tag', { tag = tag })
|
|
||||||
visit(tag.children)
|
|
||||||
end,
|
|
||||||
component = function(Component, tag)
|
|
||||||
log('Render.mount.umount.component', { Component = Component, tag = tag })
|
|
||||||
--- @type u.renderer.FnComponentContext
|
|
||||||
local ctx = assert(tag.ctx, 'could not find context for node')
|
|
||||||
|
|
||||||
-- depth-first:
|
|
||||||
visit(ctx.children)
|
|
||||||
|
|
||||||
-- now unmount current:
|
|
||||||
ctx.phase = 'unmount'
|
|
||||||
Component(ctx)
|
|
||||||
ctx.on_change = nil
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
visit(tree)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- @param old_tree u.renderer.Tree
|
|
||||||
--- @param new_tree u.renderer.Tree
|
|
||||||
--- @return u.renderer.Tree
|
|
||||||
function H2.visit_tree(old_tree, new_tree)
|
|
||||||
local old_tree_kind = Renderer.tree_kind(old_tree)
|
|
||||||
local new_tree_kind = Renderer.tree_kind(new_tree)
|
|
||||||
log('Renderer.mount.visit_tree', {
|
|
||||||
old_tree_kind = old_tree_kind,
|
|
||||||
old_tree = old_tree,
|
|
||||||
new_tree_kind = new_tree_kind,
|
|
||||||
new_tree = new_tree,
|
|
||||||
})
|
|
||||||
|
|
||||||
local new_tree_rendered = Renderer.tree_match(new_tree, {
|
|
||||||
string = function(s) return s end,
|
|
||||||
boolean = function(b) return b end,
|
|
||||||
nil_ = function() return nil end,
|
|
||||||
|
|
||||||
array = function(new_arr)
|
|
||||||
return H2.visit_array(old_tree --[[@as any]], new_arr)
|
|
||||||
end,
|
|
||||||
tag = function(new_tag)
|
|
||||||
log('Renderer.mount.visit_tree.tag', { new_tag = new_tag })
|
|
||||||
local old_children = old_tree_kind == new_tree_kind and old_tree.children or nil
|
|
||||||
return M.h(
|
|
||||||
new_tag.name,
|
|
||||||
new_tag.attributes,
|
|
||||||
H2.visit_tree(old_children --[[@as any]], new_tag.children --[[@as any]])
|
|
||||||
)
|
|
||||||
end,
|
|
||||||
|
|
||||||
component = function(NewC, new_tag)
|
|
||||||
--- @type { tag: u.renderer.Tag, ctx?: u.renderer.FnComponentContext } | nil
|
|
||||||
local old_component_info = Renderer.tree_match(
|
|
||||||
old_tree,
|
|
||||||
{ component = function(_, t) return { tag = t, ctx = t.ctx } end }
|
|
||||||
)
|
|
||||||
local old_tag = old_component_info and old_component_info.tag or nil
|
|
||||||
local ctx = old_component_info and old_component_info.ctx or nil
|
|
||||||
|
|
||||||
log('Renderer.mount.visit_tree.component', {
|
|
||||||
NewC = NewC,
|
|
||||||
new_tag = new_tag,
|
|
||||||
old_tag = old_tag,
|
|
||||||
})
|
|
||||||
|
|
||||||
if not ctx then
|
|
||||||
--- @type u.renderer.FnComponentContext
|
|
||||||
ctx = FnComponentContext.new(new_tag.attributes, nil, new_tag.children)
|
|
||||||
else
|
|
||||||
ctx.phase = 'update'
|
|
||||||
end
|
|
||||||
ctx.props = new_tag.attributes
|
|
||||||
ctx.children = new_tag.children
|
|
||||||
ctx.on_change = rerender
|
|
||||||
|
|
||||||
new_tag.ctx = ctx
|
|
||||||
local NewC_rendered_children = NewC(ctx)
|
|
||||||
local result = H2.visit_tree(ctx.prev_rendered_children, NewC_rendered_children)
|
|
||||||
ctx.prev_rendered_children = NewC_rendered_children
|
|
||||||
return result
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
if old_tree_kind ~= new_tree_kind then H2.unmount(old_tree) end
|
|
||||||
|
|
||||||
return new_tree_rendered
|
|
||||||
end
|
|
||||||
|
|
||||||
--- @param old_arr u.renderer.Node[]
|
|
||||||
--- @param new_arr u.renderer.Node[]
|
|
||||||
--- @return u.renderer.Node[]
|
|
||||||
function H2.visit_array(old_arr, new_arr)
|
|
||||||
-- We are going to hijack levenshtein in order to compute the
|
|
||||||
-- difference between elements/components. In this model, we need to
|
|
||||||
-- "update" all the nodes, so no nodes are equal. We will rely on
|
|
||||||
-- levenshtein to find the "shortest path" to conforming old => new via
|
|
||||||
-- the cost.of_change function. That will provide the meat of modeling
|
|
||||||
-- what effort it will take to morph one element into the new form.
|
|
||||||
-- What levenshtein gives us for free in this model is also informing
|
|
||||||
-- us what needs to be added (i.e., "mounted"), what needs to be
|
|
||||||
-- deleted ("unmounted") and what needs to be changed ("updated").
|
|
||||||
local changes = H.levenshtein {
|
|
||||||
from = old_arr or {},
|
|
||||||
to = new_arr or {},
|
|
||||||
are_equal = function() return false end,
|
|
||||||
cost = {
|
|
||||||
of_change = function(tree1, tree2, tree1_idx, tree2_idx)
|
|
||||||
--- @return string
|
|
||||||
local function verbose_tree_kind(tree, idx)
|
|
||||||
return Renderer.tree_match(tree, {
|
|
||||||
nil_ = function() return 'nil' end,
|
|
||||||
string = function() return 'string' end,
|
|
||||||
boolean = function() return 'boolean' end,
|
|
||||||
array = function() return 'array' end,
|
|
||||||
tag = function(tag)
|
|
||||||
return ('tag-%s-%s'):format(tag.name, tostring(tag.attributes.key or idx))
|
|
||||||
end,
|
|
||||||
component = function(C, tag)
|
|
||||||
return ('component-%s-%s'):format(C, tostring(tag.attributes.key or idx))
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
local tree1_inf = verbose_tree_kind(tree1, tree1_idx)
|
|
||||||
local tree2_inf = verbose_tree_kind(tree2, tree2_idx)
|
|
||||||
return tree1_inf == tree2_inf and 1 or 2
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
--- @type u.renderer.Node[]
|
|
||||||
local resulting_nodes = {}
|
|
||||||
for _, change in ipairs(changes) do
|
|
||||||
local resulting_node
|
|
||||||
if change.kind == 'add' then
|
|
||||||
resulting_node = H2.visit_tree(nil, change.item)
|
|
||||||
elseif change.kind == 'delete' then
|
|
||||||
H2.visit_tree(change.item, nil)
|
|
||||||
elseif change.kind == 'change' then
|
|
||||||
resulting_node = H2.visit_tree(change.from, change.to)
|
|
||||||
end
|
|
||||||
if resulting_node then table.insert(resulting_nodes, 1, resulting_node) end
|
|
||||||
end
|
|
||||||
|
|
||||||
log('Renderer.mount.visit_array', {
|
|
||||||
old_arr = old_arr,
|
|
||||||
new_arr = new_arr,
|
|
||||||
changes = changes,
|
|
||||||
resulting_nodes = resulting_nodes,
|
|
||||||
})
|
|
||||||
|
|
||||||
return resulting_nodes
|
|
||||||
end
|
|
||||||
|
|
||||||
local renderable_tree = H2.visit_tree(self.component_tree.old, tree)
|
|
||||||
self.component_tree.old = tree
|
|
||||||
self:render(renderable_tree)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Kick off initial render:
|
|
||||||
rerender()
|
|
||||||
end -- }}}
|
|
||||||
|
|
||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
-- TreeBuilder {{{
|
-- TreeBuilder {{{
|
||||||
@@ -1018,21 +728,14 @@ function TreeBuilder:tree() return self.nodes end
|
|||||||
|
|
||||||
-- Levenshtein utility {{{
|
-- Levenshtein utility {{{
|
||||||
-- luacheck: ignore
|
-- luacheck: ignore
|
||||||
--- @alias u.renderer.LevenshteinChange<T> ({ kind: 'add', item: T, index: integer } | { kind: 'delete', item: T, index: integer } | { kind: 'change', from: T, to: T, index: integer })
|
--- @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
|
--- @private
|
||||||
--- @generic T
|
--- @generic T
|
||||||
--- @param opts {
|
--- @param x `T`[]
|
||||||
--- from: `T`[],
|
--- @param y T[]
|
||||||
--- to: T[],
|
--- @param cost? { of_delete?: (fun(x: T): number), of_add?: (fun(x: T): number), of_change?: (fun(x: T, y: T): number) }
|
||||||
--- are_equal?: (fun(x: T, y: T, xidx: integer, yidx: integer): boolean),
|
|
||||||
--- cost?: {
|
|
||||||
--- of_delete?: (fun(x: T, idx: integer): number),
|
|
||||||
--- of_add?: (fun(x: T, idx: integer): number),
|
|
||||||
--- of_change?: (fun(x: T, y: T, xidx: integer, yidx: integer): number)
|
|
||||||
--- }
|
|
||||||
--- }
|
|
||||||
--- @return u.renderer.LevenshteinChange<T>[] The changes, from last (greatest index) to first (smallest index).
|
--- @return u.renderer.LevenshteinChange<T>[] The changes, from last (greatest index) to first (smallest index).
|
||||||
function H.levenshtein(opts)
|
function H.levenshtein(x, y, cost)
|
||||||
-- At the moment, this whole `cost` plumbing is not used. Deletes have the
|
-- At the moment, this whole `cost` plumbing is not used. Deletes have the
|
||||||
-- same cost as Adds or Changes. I can imagine a future, however, where
|
-- same cost as Adds or Changes. I can imagine a future, however, where
|
||||||
-- fudging with the costs of operations produces a more optimized change-set
|
-- fudging with the costs of operations produces a more optimized change-set
|
||||||
@@ -1042,13 +745,12 @@ function H.levenshtein(opts)
|
|||||||
-- in here even though it's not actively used. Hopefully having this
|
-- in here even though it's not actively used. Hopefully having this
|
||||||
-- callback-based plumbing does not cause too much of a performance hit to
|
-- callback-based plumbing does not cause too much of a performance hit to
|
||||||
-- the renderer.
|
-- the renderer.
|
||||||
if not opts.are_equal then opts.are_equal = function(x, y) return x == y end end
|
cost = cost or {}
|
||||||
if not opts.cost then opts.cost = {} end
|
local cost_of_delete_f = cost.of_delete or function() return 1 end
|
||||||
if not opts.cost.of_add then opts.cost.of_add = function() return 1 end end
|
local cost_of_add_f = cost.of_add or function() return 1 end
|
||||||
if not opts.cost.of_change then opts.cost.of_change = function() return 1 end end
|
local cost_of_change_f = cost.of_change or function() return 1 end
|
||||||
if not opts.cost.of_delete then opts.cost.of_delete = function() return 1 end end
|
|
||||||
|
|
||||||
local m, n = #opts.from, #opts.to
|
local m, n = #x, #y
|
||||||
-- Initialize the distance matrix
|
-- Initialize the distance matrix
|
||||||
local dp = {}
|
local dp = {}
|
||||||
for i = 0, m do
|
for i = 0, m do
|
||||||
@@ -1069,12 +771,12 @@ function H.levenshtein(opts)
|
|||||||
-- Compute the Levenshtein distance dynamically
|
-- Compute the Levenshtein distance dynamically
|
||||||
for i = 1, m do
|
for i = 1, m do
|
||||||
for j = 1, n do
|
for j = 1, n do
|
||||||
if opts.are_equal(opts.from[i], opts.to[j], i, j) then
|
if x[i] == y[j] then
|
||||||
dp[i][j] = dp[i - 1][j - 1] -- no cost if items are the same
|
dp[i][j] = dp[i - 1][j - 1] -- no cost if items are the same
|
||||||
else
|
else
|
||||||
local cost_delete = dp[i - 1][j] + opts.cost.of_delete(opts.from[i], i)
|
local cost_delete = dp[i - 1][j] + cost_of_delete_f(x[i])
|
||||||
local cost_add = dp[i][j - 1] + opts.cost.of_add(opts.to[j], j)
|
local cost_add = dp[i][j - 1] + cost_of_add_f(y[j])
|
||||||
local cost_change = dp[i - 1][j - 1] + opts.cost.of_change(opts.from[i], opts.to[j], i, j)
|
local cost_change = dp[i - 1][j - 1] + cost_of_change_f(x[i], y[j])
|
||||||
dp[i][j] = math.min(cost_delete, cost_add, cost_change)
|
dp[i][j] = math.min(cost_delete, cost_add, cost_change)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1099,9 +801,9 @@ function H.levenshtein(opts)
|
|||||||
|
|
||||||
if is_first_min(cost_of_change, cost_of_add, cost_of_delete) then
|
if is_first_min(cost_of_change, cost_of_add, cost_of_delete) then
|
||||||
-- potential change
|
-- potential change
|
||||||
if not opts.are_equal(opts.from[i], opts.to[j]) then
|
if x[i] ~= y[j] then
|
||||||
--- @type u.renderer.LevenshteinChange
|
--- @type u.renderer.LevenshteinChange
|
||||||
local change = { kind = 'change', from = opts.from[i], index = i, to = opts.to[j] }
|
local change = { kind = 'change', from = x[i], index = i, to = y[j] }
|
||||||
table.insert(changes, change)
|
table.insert(changes, change)
|
||||||
end
|
end
|
||||||
i = i - 1
|
i = i - 1
|
||||||
@@ -1109,13 +811,13 @@ function H.levenshtein(opts)
|
|||||||
elseif is_first_min(cost_of_add, cost_of_change, cost_of_delete) then
|
elseif is_first_min(cost_of_add, cost_of_change, cost_of_delete) then
|
||||||
-- addition
|
-- addition
|
||||||
--- @type u.renderer.LevenshteinChange
|
--- @type u.renderer.LevenshteinChange
|
||||||
local change = { kind = 'add', item = opts.to[j], index = i + 1 }
|
local change = { kind = 'add', item = y[j], index = i + 1 }
|
||||||
table.insert(changes, change)
|
table.insert(changes, change)
|
||||||
j = j - 1
|
j = j - 1
|
||||||
elseif is_first_min(cost_of_delete, cost_of_change, cost_of_add) then
|
elseif is_first_min(cost_of_delete, cost_of_change, cost_of_add) then
|
||||||
-- deletion
|
-- deletion
|
||||||
--- @type u.renderer.LevenshteinChange
|
--- @type u.renderer.LevenshteinChange
|
||||||
local change = { kind = 'delete', item = opts.from[i], index = i }
|
local change = { kind = 'delete', item = x[i], index = i }
|
||||||
table.insert(changes, change)
|
table.insert(changes, change)
|
||||||
i = i - 1
|
i = i - 1
|
||||||
else
|
else
|
||||||
|
|||||||
107
mise.toml
Normal file
107
mise.toml
Normal 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
1
nvimv
Submodule
Submodule nvimv added at bd5c243b96
6
scripts/env.sh
Normal file
6
scripts/env.sh
Normal 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)
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
pkgs ?
|
pkgs ?
|
||||||
import
|
import
|
||||||
# nixos-unstable (neovim@0.11.4):
|
# nixos-25.05 (neovim@0.11.2):
|
||||||
(fetchTarball {
|
(fetchTarball {
|
||||||
url = "https://github.com/nixos/nixpkgs/archive/0b4defa2584313f3b781240b29d61f6f9f7e0df3.tar.gz";
|
url = "https://github.com/nixos/nixpkgs/archive/1c1c9b3f5ec0421eaa0f22746295466ee6a8d48f.tar.gz";
|
||||||
sha256 = "0p3rrd8wwlk0iwgzm7frkw1k98ywrh0avi7fqjjk87i68n3inxrs";
|
sha256 = "0szvq1swpzyjmyyw929ngxy1khdnd9ba96qds2bm6l6kg4iq3cq0";
|
||||||
})
|
})
|
||||||
{ },
|
{ },
|
||||||
}:
|
}:
|
||||||
@@ -12,7 +12,7 @@ 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
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
--- @diagnostic disable: undefined-field, need-check-nil
|
||||||
local Buffer = require 'u.buffer'
|
local Buffer = require 'u.buffer'
|
||||||
local withbuf = loadfile './spec/withbuf.lua'()
|
local withbuf = loadfile './spec/withbuf.lua'()
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
--- @diagnostic disable: undefined-field, need-check-nil, need-check-nil
|
||||||
local CodeWriter = require 'u.codewriter'
|
local CodeWriter = require 'u.codewriter'
|
||||||
|
|
||||||
describe('CodeWriter', function()
|
describe('CodeWriter', function()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
--- @diagnostic disable: undefined-field, need-check-nil
|
||||||
local Pos = require 'u.pos'
|
local Pos = require 'u.pos'
|
||||||
local withbuf = loadfile './spec/withbuf.lua'()
|
local withbuf = loadfile './spec/withbuf.lua'()
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
--- @diagnostic disable: undefined-field, need-check-nil
|
||||||
local Pos = require 'u.pos'
|
local Pos = require 'u.pos'
|
||||||
local Range = require 'u.range'
|
local Range = require 'u.range'
|
||||||
local withbuf = loadfile './spec/withbuf.lua'()
|
local withbuf = loadfile './spec/withbuf.lua'()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
--- @diagnostic disable: undefined-field, need-check-nil
|
||||||
local R = require 'u.renderer'
|
local R = require 'u.renderer'
|
||||||
local withbuf = loadfile './spec/withbuf.lua'()
|
local withbuf = loadfile './spec/withbuf.lua'()
|
||||||
|
|
||||||
@@ -294,99 +295,4 @@ describe('Renderer', function()
|
|||||||
assert.are.same(bounds, { start = { 0, 9 }, stop = { 0, 19 } })
|
assert.are.same(bounds, { start = { 0, 9 }, stop = { 0, 19 } })
|
||||||
end)
|
end)
|
||||||
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,3 +1,4 @@
|
|||||||
|
--- @diagnostic disable: undefined-field, need-check-nil
|
||||||
local tracker = require 'u.tracker'
|
local tracker = require 'u.tracker'
|
||||||
local Signal = tracker.Signal
|
local Signal = tracker.Signal
|
||||||
local ExecutionContext = tracker.ExecutionContext
|
local ExecutionContext = tracker.ExecutionContext
|
||||||
|
|||||||
16
u-0.0.0-0.rockspec
Normal file
16
u-0.0.0-0.rockspec
Normal 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',
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user