Compare commits
5 Commits
v2
...
362d9e905d
| Author | SHA1 | Date | |
|---|---|---|---|
| 362d9e905d | |||
| 3fe84197c1 | |||
| 87930bf3af | |||
| 0ee6caa7ba | |||
| d9bb01be8b |
13
.busted
13
.busted
@@ -1,13 +0,0 @@
|
||||
return {
|
||||
_all = {
|
||||
coverage = false,
|
||||
lpath = "lua/?.lua;lua/?/init.lua",
|
||||
lua = "nlua",
|
||||
},
|
||||
default = {
|
||||
verbose = true
|
||||
},
|
||||
tests = {
|
||||
verbose = true
|
||||
},
|
||||
}
|
||||
25
.github/workflows/ci.yaml
vendored
25
.github/workflows/ci.yaml
vendored
@@ -2,28 +2,15 @@
|
||||
name: NeoVim tests
|
||||
on: [push]
|
||||
jobs:
|
||||
code-quality:
|
||||
plenary-tests:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
XDG_CONFIG_HOME: ${{ github.workspace }}/.config/
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v31
|
||||
- uses: rhysd/action-setup-vim@v1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
|
||||
- name: Populate Nix store
|
||||
run:
|
||||
nix-shell --run 'true'
|
||||
|
||||
- name: Type-check with lua-language-server
|
||||
run:
|
||||
nix-shell --run 'make lint'
|
||||
|
||||
- name: Check formatting with stylua
|
||||
run:
|
||||
nix-shell --run 'make fmt-check'
|
||||
|
||||
- name: Run busted tests
|
||||
run:
|
||||
nix-shell --run 'make test'
|
||||
neovim: true
|
||||
version: v0.11.0
|
||||
arch: 'x86_64'
|
||||
- run: make test
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1,3 @@
|
||||
/.lux/
|
||||
lux.lock
|
||||
*.src.rock
|
||||
*.aider*
|
||||
luacov.*.out
|
||||
|
||||
27
Makefile
27
Makefile
@@ -1,21 +1,16 @@
|
||||
all: lint fmt-check test
|
||||
PLENARY_DIR=~/.local/share/nvim/site/pack/test/opt/plenary.nvim
|
||||
|
||||
all: lint test
|
||||
|
||||
lint:
|
||||
@echo "## Typechecking"
|
||||
@lua-language-server --check=lua/u/ --checklevel=Error
|
||||
|
||||
fmt-check:
|
||||
@echo "## Checking code format"
|
||||
@stylua --check .
|
||||
lua-language-server --check=lua/u/ --checklevel=Hint
|
||||
lux check
|
||||
|
||||
fmt:
|
||||
@echo "## Formatting code"
|
||||
@stylua .
|
||||
stylua .
|
||||
|
||||
test:
|
||||
@rm -f luacov.*.out
|
||||
@echo "## Running tests"
|
||||
@busted --coverage --verbose
|
||||
@echo "## Generating coverage report"
|
||||
@luacov
|
||||
@awk '/^Summary$$/{flag=1;next} flag{print}' luacov.report.out
|
||||
test: $(PLENARY_DIR)
|
||||
NVIM_APPNAME=noplugstest nvim -u NORC --headless -c 'set packpath+=~/.local/share/nvim/site' -c 'packadd plenary.nvim' -c "PlenaryBustedDirectory spec/"
|
||||
|
||||
$(PLENARY_DIR):
|
||||
git clone https://github.com/nvim-lua/plenary.nvim/ $(PLENARY_DIR)
|
||||
|
||||
119
README.md
119
README.md
@@ -1,40 +1,26 @@
|
||||
# u.nvim
|
||||
|
||||
Welcome to **u.nvim** - a powerful Lua library designed to enhance your text
|
||||
manipulation experience in NeoVim, focusing on text-manipulation utilities.
|
||||
This includes a `Range` utility, allowing you to work efficiently with text
|
||||
selections based on various conditions, as well as a declarative `Render`-er,
|
||||
making coding and editing more intuitive and productive.
|
||||
Welcome to **u.nvim** – a powerful Lua library designed to enhance your text manipulation experience in NeoVim, focusing primarily on a context-aware "Range" utility. This utility allows you to work efficiently with text selections based on various conditions, in a variety of contexts, making coding and editing more intuitive and productive.
|
||||
|
||||
This is meant to be used as a **library**, not a plugin. On its own, `u.nvim`
|
||||
does nothing. It is meant to be used by plugin authors, to make their lives
|
||||
easier based on the variety of utilities I found I needed while growing my
|
||||
NeoVim config. To get an idea of what a plugin built on top of `u.nvim` would
|
||||
look like, check out the [examples/](./examples/) directory.
|
||||
This is meant to be used as a **library**, not a plugin. On its own, `u.nvim` does nothing. It is meant to be used by plugin authors, to make their lives easier based on the variety of utilities I found I needed while growing my NeoVim config. To get an idea of what a plugin built on top of `u.nvim` would look like, check out the [examples/](./examples/) directory.
|
||||
|
||||
## Features
|
||||
|
||||
- **Rendering System**: a utility that can declaratively render NeoVim-specific
|
||||
hyperscript into a buffer, supporting creating/managing extmarks, highlights,
|
||||
and key-event handling (requires NeoVim >0.11)
|
||||
- **Signals**: a simple dependency tracking system that pairs well with the
|
||||
rendering utilities for creating reactive/interactive UIs in NeoVim.
|
||||
- **Range Utility**: Get context-aware selections with ease. Replace regions
|
||||
with new text. Think of it as a programmatic way to work with visual
|
||||
selections (or regions of text).
|
||||
- **Rendering System**: a utility that can declaratively render NeoVim-specific hyperscript into a buffer, supporting creating/managing extmarks, highlights, and key-event handling (requires NeoVim >0.11)
|
||||
- **Signals**: a simple dependency tracking system that pairs well with the rendering utilities for creating reactive/interactive UIs in NeoVim.
|
||||
- **Range Utility**: Get context-aware selections with ease. Replace regions with new text. Think of it as a programmatic way to work with visual selections (or regions of text).
|
||||
- **Code Writer**: Write code with automatic indentation and formatting.
|
||||
- **Operator Key Mapping**: Flexible key mapping that works with the selected
|
||||
text.
|
||||
- **Text and Position Utilities**: Convenient functions to manage text objects
|
||||
and cursor positions.
|
||||
- **Operator Key Mapping**: Flexible key mapping that works with the selected text.
|
||||
- **Text and Position Utilities**: Convenient functions to manage text objects and cursor positions.
|
||||
|
||||
### Installation
|
||||
|
||||
This being a library, and not a proper plugin, it is recommended that you
|
||||
vendor the specific version of this library that you need, including it in your
|
||||
code. Package managers are a developing landscape for Lua in the context of
|
||||
NeoVim. Perhaps in the future, `lux` will eliminate the need to vendor this
|
||||
library in your application code.
|
||||
lazy.nvim:
|
||||
```lua
|
||||
-- Setting `lazy = true` ensures that the library is only loaded
|
||||
-- when `require 'u.<utility>' is called.
|
||||
{ 'jrop/u.nvim', lazy = true }
|
||||
```
|
||||
|
||||
## Signal and Rendering Usage
|
||||
|
||||
@@ -125,9 +111,7 @@ end)
|
||||
|
||||
### `u.tracker`
|
||||
|
||||
The `u.tracker` module provides a simple API for creating reactive variables.
|
||||
These can be composed in Effects and Memos utilizing Execution Contexts that
|
||||
track what signals are used by effects/memos.
|
||||
The `u.tracker` module provides a simple API for creating reactive variables. These can be composed in Effects and Memos utilizing Execution Contexts that track what signals are used by effects/memos.
|
||||
|
||||
```lua
|
||||
local tracker = require('u.tracker')
|
||||
@@ -150,8 +134,7 @@ The renderer library renders hyperscript into a buffer. Each render performs a
|
||||
minimal set of changes in order to transform the current buffer text into the
|
||||
desired state.
|
||||
|
||||
**Hyperscript** is just 1) _text_ 2) `<text>` tags, which can be nested in 3)
|
||||
Lua tables for readability:
|
||||
**Hyperscript** is just 1) _text_ 2) `<text>` tags, which can be nested in 3) Lua tables for readability:
|
||||
|
||||
```lua
|
||||
local h = require('u.renderer').h
|
||||
@@ -213,8 +196,7 @@ renderer:render(
|
||||
)
|
||||
```
|
||||
|
||||
**Rendering**: The renderer library provides a `render` function that takes
|
||||
hyperscript in, and converts it to formatted buffer text:
|
||||
**Rendering**: The renderer library provides a `render` function that takes hyperscript in, and converts it to formatted buffer text:
|
||||
|
||||
```lua
|
||||
local Renderer = require('u.renderer').Renderer
|
||||
@@ -237,37 +219,22 @@ buf:render {
|
||||
|
||||
<blockquote>
|
||||
<del>
|
||||
I love NeoVim. I am coming to love Lua. I don't like 1-based indices; perhaps I
|
||||
am too old. Perhaps I am too steeped in the history of loving the elegance of
|
||||
simple pointer arithmetic. Regardless, the way positions are addressed in
|
||||
NeoVim/Vim is (terrifyingly) mixed. Some methods return 1-based, others accept
|
||||
only 0-based. In order to stay sane, I had to make a choice to store everything
|
||||
in one, uniform representation in this library. I chose (what I humbly think is
|
||||
the only sane way) to stick with the tried-and-true 0-based index scheme. That
|
||||
abstraction leaks into the public API of this library.
|
||||
I love NeoVim. I am coming to love Lua. I don't like 1-based indices; perhaps I am too old. Perhaps I am too steeped in the history of loving the elegance of simple pointer arithmetic. Regardless, the way positions are addressed in NeoVim/Vim is (terrifyingly) mixed. Some methods return 1-based, others accept only 0-based. In order to stay sane, I had to make a choice to store everything in one, uniform representation in this library. I chose (what I humbly think is the only sane way) to stick with the tried-and-true 0-based index scheme. That abstraction leaks into the public API of this library.
|
||||
</del>
|
||||
</blockquote>
|
||||
|
||||
<br />
|
||||
<b>This has changed in v2</b>. After much thought, I realized that:
|
||||
|
||||
1. The 0-based indexing in NeoVim is prevelant in the `:api`, which is designed
|
||||
to be exposed to many languages. As such, it makes sense for this interface
|
||||
to use 0-based indexing. However, many internal Vim functions use 1-based
|
||||
indexing.
|
||||
2. This is a Lua library (surprise, surprise, duh) - the idioms of the language
|
||||
should take precedence over my preference
|
||||
3. There were subtle bugs in the code where indices weren't being normalized to
|
||||
0-based, anyways. Somehow it worked most of the time.
|
||||
1. The 0-based indexing in NeoVim is prevelant in the `:api`, which is designed to be exposed to many languages. As such, it makes sense for this interface to use 0-based indexing. However, many internal Vim functions use 1-based indexing.
|
||||
2. This is a Lua library (surprise, surprise, duh) - the idioms of the language should take precedence over my preference
|
||||
3. There were subtle bugs in the code where indices weren't being normalized to 0-based, anyways. Somehow it worked most of the time.
|
||||
|
||||
As such, this library now uses 1-based indexing everywhere, doing the necessary
|
||||
interop conversions when calling `:api` functions.
|
||||
As such, this library now uses 1-based indexing everywhere, doing the necessary interop conversions when calling `:api` functions.
|
||||
|
||||
### 1. Creating a Range
|
||||
|
||||
The `Range` utility is the main feature upon which most other things in this
|
||||
library are built, aside from a few standalone utilities. Ranges can be
|
||||
constructed manually, or preferably, obtained based on a variety of contexts.
|
||||
The `Range` utility is the main feature upon which most other things in this library are built, aside from a few standalone utilities. Ranges can be constructed manually, or preferably, obtained based on a variety of contexts.
|
||||
|
||||
```lua
|
||||
local Range = require 'u.range'
|
||||
@@ -278,9 +245,7 @@ Range.new(start, stop, 'v') -- charwise selection
|
||||
Range.new(start, stop, 'V') -- linewise selection
|
||||
```
|
||||
|
||||
This is usually not how you want to obtain a `Range`, however. Usually you want
|
||||
to get the corresponding context of an edit operation and just "get me the
|
||||
current Range that represents this context".
|
||||
This is usually not how you want to obtain a `Range`, however. Usually you want to get the corresponding context of an edit operation and just "get me the current Range that represents this context".
|
||||
|
||||
```lua
|
||||
-- get the first line in a buffer:
|
||||
@@ -295,8 +260,7 @@ Range.from_motion('iW')
|
||||
Range.from_motion('a"')
|
||||
|
||||
-- Get the currently visually selected text:
|
||||
-- NOTE: this does NOT work within certain contexts; more specialized utilities
|
||||
-- are more appropriate in certain circumstances
|
||||
-- NOTE: this does NOT work within certain contexts; more specialized utilities are more appropriate in certain circumstances
|
||||
Range.from_vtext()
|
||||
|
||||
--
|
||||
@@ -308,8 +272,7 @@ function MyOpFunc(ty)
|
||||
local range = Range.from_op_func(ty)
|
||||
-- do something with the range
|
||||
end
|
||||
-- Try invoking this with: `<Leader>toaw`, and the current word will be the
|
||||
-- context:
|
||||
-- Try invoking this with: `<Leader>toaw`, and the current word will be the context:
|
||||
vim.keymap.set('<Leader>to', function()
|
||||
vim.g.operatorfunc = 'v:lua.MyOpFunc'
|
||||
return 'g@'
|
||||
@@ -318,8 +281,7 @@ end, { expr = true })
|
||||
--
|
||||
-- Commands:
|
||||
--
|
||||
-- When executing commands in a visual context, getting the selected text has
|
||||
-- to be done differently:
|
||||
-- When executing commands in a visual context, getting the selected text has to be done differently:
|
||||
vim.api.nvim_create_user_command('MyCmd', function(args)
|
||||
local range = Range.from_cmd_args(args)
|
||||
if range == nil then
|
||||
@@ -330,8 +292,7 @@ vim.api.nvim_create_user_command('MyCmd', function(args)
|
||||
end, { range = true })
|
||||
```
|
||||
|
||||
So far, that's a lot of ways to _get_ a `Range`. But what can you do with a
|
||||
range once you have one? Plenty, it turns out!
|
||||
So far, that's a lot of ways to _get_ a `Range`. But what can you do with a range once you have one? Plenty, it turns out!
|
||||
|
||||
```lua
|
||||
local range = ...
|
||||
@@ -401,11 +362,11 @@ Access and manipulate buffers easily:
|
||||
```lua
|
||||
local Buffer = require 'u.buffer'
|
||||
local buf = Buffer.current()
|
||||
buf.b.<option> -- get buffer-local variables
|
||||
buf.b.<option> = ... -- set buffer-local variables
|
||||
buf.bo.<option> -- get buffer options
|
||||
buf.bo.<option> = ... -- set buffer options
|
||||
buf:line_count() -- the number of lines in the current buffer
|
||||
buf:get_option '...'
|
||||
buf:set_option('...', ...)
|
||||
buf:get_var '...'
|
||||
buf:set_var('...', ...)
|
||||
buf:all() -- returns a Range representing the entire buffer
|
||||
buf:is_empty() -- returns true if the buffer has no text
|
||||
buf:append_line '...'
|
||||
@@ -420,20 +381,8 @@ buf:txtobj('iw') -- returns a Range representing the text object 'iw' in th
|
||||
|
||||
Copyright (c) 2024 jrapodaca@gmail.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
@@ -44,7 +44,11 @@ end
|
||||
|
||||
--- Normalizes the given path to an absolute path.
|
||||
--- @param path string
|
||||
function H.normalize(path) return vim.fs.abspath(vim.fs.normalize(path)) end
|
||||
function H.normalize(path)
|
||||
path = vim.fs.normalize(path)
|
||||
if path:sub(1, 1) ~= '/' then path = vim.fs.joinpath(vim.uv.cwd(), path) end
|
||||
return vim.fs.normalize(path)
|
||||
end
|
||||
|
||||
--- Computes the relative path from `base` to `path`.
|
||||
--- @param path string
|
||||
@@ -121,10 +125,7 @@ end
|
||||
---
|
||||
--- @return { expand: fun(path: string), collapse: fun(path: string) }
|
||||
local function _render_in_buffer(opts)
|
||||
local winnr = vim.api.nvim_buf_call(
|
||||
opts.bufnr,
|
||||
function() return vim.api.nvim_get_current_win() end
|
||||
)
|
||||
local winnr = vim.api.nvim_buf_call(opts.bufnr, function() return vim.api.nvim_get_current_win() end)
|
||||
local s_tree_inf = tracker.create_signal(H.get_tree_inf(opts.root_path))
|
||||
local s_focused_path = tracker.create_signal(H.normalize(opts.focus_path or opts.root_path))
|
||||
|
||||
@@ -154,35 +155,19 @@ local function _render_in_buffer(opts)
|
||||
end)
|
||||
end)
|
||||
|
||||
-- --
|
||||
-- -- TODO: :help watch-file
|
||||
-- --
|
||||
-- local watcher = vim.uv.new_fs_event()
|
||||
-- if watcher ~= nil then
|
||||
-- watcher:start(root_path, { recursive = true }, function(err, fname, status)
|
||||
-- -- TODO: more efficient update:
|
||||
-- s_tree_inf:set(H.get_tree(root_path))
|
||||
--
|
||||
-- :help watch-file
|
||||
--
|
||||
local watcher = vim.uv.new_fs_event()
|
||||
if watcher ~= nil then
|
||||
--- @diagnostic disable-next-line: unused-local
|
||||
watcher:start(opts.root_path, { recursive = true }, function(_err, fname, _status)
|
||||
fname = H.normalize(fname)
|
||||
|
||||
local dir_path = vim.fs.dirname(fname)
|
||||
local dir = s_tree_inf:get().path_to_node[dir_path]
|
||||
if not dir then return end
|
||||
|
||||
s_tree_inf:schedule_update(function(tree_inf)
|
||||
H.populate_dir_children(dir, tree_inf.path_to_node)
|
||||
return tree_inf
|
||||
end)
|
||||
end)
|
||||
end
|
||||
vim.api.nvim_create_autocmd('WinClosed', {
|
||||
once = true,
|
||||
pattern = tostring(winnr),
|
||||
callback = function()
|
||||
if watcher == nil then return end
|
||||
|
||||
watcher:stop()
|
||||
watcher = nil
|
||||
end,
|
||||
})
|
||||
-- -- TODO: proper disposal
|
||||
-- watcher:stop()
|
||||
-- end)
|
||||
-- end
|
||||
|
||||
local controller = {}
|
||||
|
||||
@@ -346,11 +331,7 @@ local function _render_in_buffer(opts)
|
||||
return ''
|
||||
end,
|
||||
n = function()
|
||||
vim.schedule(
|
||||
function()
|
||||
controller.new(node.kind == 'file' and vim.fs.dirname(node.path) or node.path)
|
||||
end
|
||||
)
|
||||
vim.schedule(function() controller.new(node.kind == 'file' and vim.fs.dirname(node.path) or node.path) end)
|
||||
return ''
|
||||
end,
|
||||
r = function()
|
||||
@@ -370,11 +351,7 @@ local function _render_in_buffer(opts)
|
||||
local icon = node.expanded and '' or ''
|
||||
tb:put {
|
||||
current_line > 1 and '\n',
|
||||
h(
|
||||
'text',
|
||||
{ hl = 'Constant', nmap = nmaps },
|
||||
{ string.rep(' ', level), icon, ' ', name }
|
||||
),
|
||||
h('text', { hl = 'Constant', nmap = nmaps }, { string.rep(' ', level), icon, ' ', name }),
|
||||
}
|
||||
if node.expanded then
|
||||
for _, child in ipairs(node.children) do
|
||||
@@ -434,8 +411,8 @@ function M.show(opts)
|
||||
callback = M.hide,
|
||||
})
|
||||
|
||||
vim.wo[0][0].number = false
|
||||
vim.wo[0][0].relativenumber = false
|
||||
vim.wo.number = false
|
||||
vim.wo.relativenumber = false
|
||||
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
local Renderer = require('u.renderer').Renderer
|
||||
local Buffer = require 'u.buffer'
|
||||
local TreeBuilder = require('u.renderer').TreeBuilder
|
||||
local tracker = require 'u.tracker'
|
||||
local utils = require 'u.utils'
|
||||
local Window = require 'my.window'
|
||||
|
||||
local TIMEOUT = 4000
|
||||
local ICONS = {
|
||||
@@ -13,25 +14,15 @@ local ICONS = {
|
||||
}
|
||||
local DEFAULT_ICON = { text = '', group = 'DiagnosticSignOk' }
|
||||
|
||||
local S_EDITOR_DIMENSIONS =
|
||||
tracker.create_signal(utils.get_editor_dimensions(), 's:editor_dimensions')
|
||||
vim.api.nvim_create_autocmd('VimResized', {
|
||||
callback = function()
|
||||
local new_dim = utils.get_editor_dimensions()
|
||||
S_EDITOR_DIMENSIONS:set(new_dim)
|
||||
end,
|
||||
})
|
||||
|
||||
--- @alias u.example.Notification {
|
||||
--- @alias Notification {
|
||||
--- kind: number;
|
||||
--- id: number;
|
||||
--- text: string;
|
||||
--- timer: uv.uv_timer_t;
|
||||
--- }
|
||||
|
||||
local M = {}
|
||||
|
||||
--- @type { win: integer, buf: integer, renderer: u.Renderer } | nil
|
||||
--- @type Window | nil
|
||||
local notifs_w
|
||||
|
||||
local s_notifications_raw = tracker.create_signal {}
|
||||
@@ -39,49 +30,44 @@ local s_notifications = s_notifications_raw:debounce(50)
|
||||
|
||||
-- Render effect:
|
||||
tracker.create_effect(function()
|
||||
--- @type u.example.Notification[]
|
||||
--- @type Notification[]
|
||||
local notifs = s_notifications:get()
|
||||
--- @type { width: integer, height: integer }
|
||||
local editor_size = S_EDITOR_DIMENSIONS:get()
|
||||
|
||||
if #notifs == 0 then
|
||||
if notifs_w then
|
||||
if vim.api.nvim_win_is_valid(notifs_w.win) then vim.api.nvim_win_close(notifs_w.win, true) end
|
||||
notifs_w:close(true)
|
||||
notifs_w = nil
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
vim.schedule(function()
|
||||
local editor_size = utils.get_editor_dimensions()
|
||||
local avail_width = editor_size.width
|
||||
local float_width = 40
|
||||
local float_height = math.min(#notifs, editor_size.height - 3)
|
||||
local win_config = {
|
||||
relative = 'editor',
|
||||
anchor = 'NE',
|
||||
row = 0,
|
||||
col = avail_width,
|
||||
width = float_width,
|
||||
height = float_height,
|
||||
height = math.min(#notifs, editor_size.height - 3),
|
||||
border = 'single',
|
||||
focusable = false,
|
||||
zindex = 900,
|
||||
}
|
||||
vim.schedule(function()
|
||||
if not notifs_w or not vim.api.nvim_win_is_valid(notifs_w.win) then
|
||||
local b = vim.api.nvim_create_buf(false, true)
|
||||
local w = vim.api.nvim_open_win(b, false, win_config)
|
||||
vim.wo[w].cursorline = false
|
||||
vim.wo[w].list = false
|
||||
vim.wo[w].listchars = ''
|
||||
vim.wo[w].number = false
|
||||
vim.wo[w].relativenumber = false
|
||||
vim.wo[w].wrap = false
|
||||
notifs_w = { win = w, buf = b, renderer = Renderer.new(b) }
|
||||
notifs_w = Window.new(Buffer.create(false, true), win_config)
|
||||
vim.wo[notifs_w.win].cursorline = false
|
||||
vim.wo[notifs_w.win].list = false
|
||||
vim.wo[notifs_w.win].listchars = ''
|
||||
vim.wo[notifs_w.win].number = false
|
||||
vim.wo[notifs_w.win].relativenumber = false
|
||||
vim.wo[notifs_w.win].wrap = false
|
||||
else
|
||||
vim.api.nvim_win_set_config(notifs_w.win, win_config)
|
||||
notifs_w:set_config(win_config)
|
||||
end
|
||||
|
||||
notifs_w.renderer:render(TreeBuilder.new()
|
||||
notifs_w:render(TreeBuilder.new()
|
||||
:nest(function(tb)
|
||||
for idx, notif in ipairs(notifs) do
|
||||
if idx > 1 then tb:put '\n' end
|
||||
@@ -93,81 +79,48 @@ tracker.create_effect(function()
|
||||
end)
|
||||
:tree())
|
||||
vim.api.nvim_win_call(notifs_w.win, function()
|
||||
vim.fn.winrestview {
|
||||
-- scroll all the way left:
|
||||
leftcol = 0,
|
||||
-- set the bottom line to be at the bottom of the window:
|
||||
topline = vim.api.nvim_buf_line_count(notifs_w.buf) - win_config.height + 1,
|
||||
}
|
||||
-- scroll to bottom:
|
||||
vim.cmd.normal 'G'
|
||||
-- scroll all the way to the left:
|
||||
vim.cmd.normal '9999zh'
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
--- @param id number
|
||||
local function _delete_notif(id)
|
||||
--- @param notifs u.example.Notification[]
|
||||
local _orig_notify
|
||||
|
||||
--- @param msg string
|
||||
--- @param level integer|nil
|
||||
--- @param opts table|nil
|
||||
local function my_notify(msg, level, opts)
|
||||
vim.schedule(function() _orig_notify(msg, level, opts) end)
|
||||
if level == nil then level = vim.log.levels.INFO end
|
||||
if level < vim.log.levels.INFO then return end
|
||||
|
||||
local id = math.random(math.huge)
|
||||
|
||||
--- @param notifs Notification[]
|
||||
s_notifications_raw:schedule_update(function(notifs)
|
||||
table.insert(notifs, { kind = level, id = id, text = msg })
|
||||
return notifs
|
||||
end)
|
||||
|
||||
vim.defer_fn(function()
|
||||
--- @param notifs Notification[]
|
||||
s_notifications_raw:schedule_update(function(notifs)
|
||||
for i, notif in ipairs(notifs) do
|
||||
if notif.id == id then
|
||||
notif.timer:stop()
|
||||
notif.timer:close()
|
||||
table.remove(notifs, i)
|
||||
break
|
||||
end
|
||||
end
|
||||
return notifs
|
||||
end)
|
||||
end
|
||||
|
||||
local _orig_notify
|
||||
|
||||
--- @param msg string
|
||||
--- @param level integer|nil
|
||||
--- @param opts? { id: number }
|
||||
function M.notify(msg, level, opts)
|
||||
if level == nil then level = vim.log.levels.INFO end
|
||||
|
||||
opts = opts or {}
|
||||
local id = opts.id or math.random(999999999)
|
||||
|
||||
--- @type u.example.Notification?
|
||||
local notif = vim.iter(s_notifications_raw:get()):find(function(n) return n.id == id end)
|
||||
if not notif then
|
||||
-- Create a new notification (maybe):
|
||||
if vim.trim(msg) == '' then return id end
|
||||
if level < vim.log.levels.INFO then return id end
|
||||
|
||||
local timer = assert((vim.uv or vim.loop).new_timer(), 'could not create timer')
|
||||
timer:start(TIMEOUT, 0, function() _delete_notif(id) end)
|
||||
notif = {
|
||||
id = id,
|
||||
kind = level,
|
||||
text = msg,
|
||||
timer = timer,
|
||||
}
|
||||
--- @param notifs u.example.Notification[]
|
||||
s_notifications_raw:schedule_update(function(notifs)
|
||||
table.insert(notifs, notif)
|
||||
return notifs
|
||||
end)
|
||||
else
|
||||
-- Update an existing notification:
|
||||
s_notifications_raw:schedule_update(function(notifs)
|
||||
-- We already have a copy-by-reference of the notif we want to modify:
|
||||
notif.timer:stop()
|
||||
notif.text = msg
|
||||
notif.kind = level
|
||||
notif.timer:start(TIMEOUT, 0, function() _delete_notif(id) end)
|
||||
|
||||
return notifs
|
||||
end)
|
||||
end
|
||||
|
||||
return id
|
||||
end, TIMEOUT)
|
||||
end
|
||||
|
||||
local _once_msgs = {}
|
||||
function M.notify_once(msg, level, opts)
|
||||
local function my_notify_once(msg, level, opts)
|
||||
if vim.tbl_contains(_once_msgs, msg) then return false end
|
||||
table.insert(_once_msgs, msg)
|
||||
vim.notify(msg, level, opts)
|
||||
@@ -177,8 +130,8 @@ end
|
||||
function M.setup()
|
||||
if _orig_notify == nil then _orig_notify = vim.notify end
|
||||
|
||||
vim.notify = M.notify
|
||||
vim.notify_once = M.notify_once
|
||||
vim.notify = my_notify
|
||||
vim.notify_once = my_notify_once
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -7,8 +7,7 @@ local tracker = require 'u.tracker'
|
||||
|
||||
local M = {}
|
||||
|
||||
local S_EDITOR_DIMENSIONS =
|
||||
tracker.create_signal(utils.get_editor_dimensions(), 's:editor_dimensions')
|
||||
local S_EDITOR_DIMENSIONS = tracker.create_signal(utils.get_editor_dimensions(), 's:editor_dimensions')
|
||||
vim.api.nvim_create_autocmd('VimResized', {
|
||||
callback = function()
|
||||
local new_dim = utils.get_editor_dimensions()
|
||||
@@ -203,9 +202,7 @@ function M.create_picker(opts) -- {{{
|
||||
|
||||
local s_filter_text_undebounced = tracker.create_signal('', 's:filter_text')
|
||||
w_input_buf:autocmd('TextChangedI', {
|
||||
callback = safe_wrap(
|
||||
function() s_filter_text_undebounced:set(vim.api.nvim_get_current_line()) end
|
||||
),
|
||||
callback = safe_wrap(function() s_filter_text_undebounced:set(vim.api.nvim_get_current_line()) end),
|
||||
})
|
||||
local s_filter_text = s_filter_text_undebounced:debounce(50)
|
||||
|
||||
@@ -214,15 +211,10 @@ function M.create_picker(opts) -- {{{
|
||||
--
|
||||
|
||||
local s_formatted_items = tracker.create_memo(function()
|
||||
local function _format_item(item)
|
||||
return opts.format_item and opts.format_item(item) or tostring(item)
|
||||
end
|
||||
local function _format_item(item) return opts.format_item and opts.format_item(item) or tostring(item) end
|
||||
|
||||
local items = s_items:get()
|
||||
return vim
|
||||
.iter(items)
|
||||
:map(function(item) return { item = item, formatted = _format_item(item) } end)
|
||||
:totable()
|
||||
return vim.iter(items):map(function(item) return { item = item, formatted = _format_item(item) } end):totable()
|
||||
end)
|
||||
|
||||
-- When the filter text changes, update the filtered items:
|
||||
@@ -231,10 +223,8 @@ function M.create_picker(opts) -- {{{
|
||||
local formatted_items = s_formatted_items:get()
|
||||
local filter_text = vim.trim(s_filter_text:get()):lower()
|
||||
|
||||
--- @type string
|
||||
local filter_pattern
|
||||
--- @type boolean
|
||||
local use_plain_pattern
|
||||
local filter_pattern = ''
|
||||
local use_plain_pattern = false
|
||||
if #formatted_items > 250 and #filter_text <= 3 then
|
||||
filter_pattern = filter_text
|
||||
use_plain_pattern = true
|
||||
@@ -257,9 +247,7 @@ function M.create_picker(opts) -- {{{
|
||||
local new_filtered_items = vim
|
||||
.iter(formatted_items)
|
||||
:enumerate()
|
||||
:map(
|
||||
function(i, inf) return { orig_idx = i, item = inf.item, formatted = inf.formatted } end
|
||||
)
|
||||
:map(function(i, inf) return { orig_idx = i, item = inf.item, formatted = inf.formatted } end)
|
||||
:filter(function(inf)
|
||||
if filter_text == '' then return true end
|
||||
local formatted_as_string = Renderer.markup_to_string({ tree = inf.formatted }):lower()
|
||||
@@ -267,7 +255,9 @@ function M.create_picker(opts) -- {{{
|
||||
formatted_strings[inf.orig_idx] = formatted_as_string
|
||||
if use_plain_pattern then
|
||||
local x, y = formatted_as_string:find(filter_pattern, 1, true)
|
||||
if x ~= nil and y ~= nil then matches[inf.orig_idx] = formatted_as_string:sub(x, y) end
|
||||
if x ~= nil and y ~= nil then
|
||||
matches[inf.orig_idx] = formatted_as_string:sub(x, y)
|
||||
end
|
||||
else
|
||||
matches[inf.orig_idx] = string.match(formatted_as_string, filter_pattern)
|
||||
end
|
||||
@@ -330,9 +320,7 @@ function M.create_picker(opts) -- {{{
|
||||
local filtered_items = s_filtered_items:get()
|
||||
local cursor_index = s_cursor_index:get()
|
||||
local indices = shallow_copy_arr(selected_indices)
|
||||
if #indices == 0 and #filtered_items > 0 then
|
||||
indices = { filtered_items[cursor_index].orig_idx }
|
||||
end
|
||||
if #indices == 0 and #filtered_items > 0 then indices = { filtered_items[cursor_index].orig_idx } end
|
||||
return {
|
||||
items = vim.iter(indices):map(function(i) return items[i] end):totable(),
|
||||
indices = indices,
|
||||
@@ -409,18 +397,8 @@ function M.create_picker(opts) -- {{{
|
||||
end
|
||||
s_cursor_index:set(next_cursor_index)
|
||||
end
|
||||
vim.keymap.set(
|
||||
'i',
|
||||
'<C-n>',
|
||||
safe_wrap(action_next_line),
|
||||
{ buffer = w_input_buf.bufnr, desc = 'Picker: next' }
|
||||
)
|
||||
vim.keymap.set(
|
||||
'i',
|
||||
'<Down>',
|
||||
safe_wrap(action_next_line),
|
||||
{ buffer = w_input_buf.bufnr, desc = 'Picker: next' }
|
||||
)
|
||||
vim.keymap.set('i', '<C-n>', safe_wrap(action_next_line), { buffer = w_input_buf.bufnr, desc = 'Picker: next' })
|
||||
vim.keymap.set('i', '<Down>', safe_wrap(action_next_line), { buffer = w_input_buf.bufnr, desc = 'Picker: next' })
|
||||
|
||||
local function action_prev_line()
|
||||
local max_line = #s_filtered_items:get()
|
||||
@@ -428,18 +406,8 @@ function M.create_picker(opts) -- {{{
|
||||
if next_cursor_index - s_top_offset:get() < 1 then s_top_offset:set(s_top_offset:get() - 1) end
|
||||
s_cursor_index:set(next_cursor_index)
|
||||
end
|
||||
vim.keymap.set(
|
||||
'i',
|
||||
'<C-p>',
|
||||
safe_wrap(action_prev_line),
|
||||
{ buffer = w_input_buf.bufnr, desc = 'Picker: previous' }
|
||||
)
|
||||
vim.keymap.set(
|
||||
'i',
|
||||
'<Up>',
|
||||
safe_wrap(action_prev_line),
|
||||
{ buffer = w_input_buf.bufnr, desc = 'Picker: previous' }
|
||||
)
|
||||
vim.keymap.set('i', '<C-p>', safe_wrap(action_prev_line), { buffer = w_input_buf.bufnr, desc = 'Picker: previous' })
|
||||
vim.keymap.set('i', '<Up>', safe_wrap(action_prev_line), { buffer = w_input_buf.bufnr, desc = 'Picker: previous' })
|
||||
|
||||
vim.keymap.set(
|
||||
'i',
|
||||
@@ -449,9 +417,7 @@ function M.create_picker(opts) -- {{{
|
||||
|
||||
local index = s_filtered_items:get()[s_cursor_index:get()].orig_idx
|
||||
if vim.tbl_contains(s_selected_indices:get(), index) then
|
||||
s_selected_indices:set(
|
||||
vim.iter(s_selected_indices:get()):filter(function(i) return i ~= index end):totable()
|
||||
)
|
||||
s_selected_indices:set(vim.iter(s_selected_indices:get()):filter(function(i) return i ~= index end):totable())
|
||||
else
|
||||
local new_selected_indices = shallow_copy_arr(s_selected_indices:get())
|
||||
table.insert(new_selected_indices, index)
|
||||
@@ -463,12 +429,7 @@ function M.create_picker(opts) -- {{{
|
||||
)
|
||||
|
||||
for key, fn in pairs(opts.mappings or {}) do
|
||||
vim.keymap.set(
|
||||
'i',
|
||||
key,
|
||||
safe_wrap(function() return fn(controller) end),
|
||||
{ buffer = w_input_buf.bufnr }
|
||||
)
|
||||
vim.keymap.set('i', key, safe_wrap(function() return fn(controller) end), { buffer = w_input_buf.bufnr })
|
||||
end
|
||||
|
||||
-- Render:
|
||||
@@ -538,9 +499,10 @@ function M.create_picker(opts) -- {{{
|
||||
if ephemeral == nil then ephemeral = false end
|
||||
|
||||
if ephemeral and #indicies == 1 then
|
||||
local matching_filtered_item_idx, _ = vim.iter(s_filtered_items:get()):enumerate():find(
|
||||
function(_idx, inf) return inf.orig_idx == indicies[1] end
|
||||
)
|
||||
local matching_filtered_item_idx, _ = vim
|
||||
.iter(s_filtered_items:get())
|
||||
:enumerate()
|
||||
:find(function(_idx, inf) return inf.orig_idx == indicies[1] end)
|
||||
if matching_filtered_item_idx ~= nil then s_cursor_index:set(indicies[1]) end
|
||||
else
|
||||
if not opts.multi then
|
||||
@@ -759,10 +721,7 @@ function M.files(opts) -- {{{
|
||||
-- fast laptop. Show a warning and truncate the list in this case.
|
||||
if #lines >= opts.limit then
|
||||
if not job_inf.notified_over_limit then
|
||||
vim.notify(
|
||||
'Picker list is too large (truncating list to ' .. opts.limit .. ' items)',
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
vim.notify('Picker list is too large (truncating list to ' .. opts.limit .. ' items)', vim.log.levels.WARN)
|
||||
pcall(vim.fn.jobstop, job_inf.id)
|
||||
job_inf.notified_over_limit = true
|
||||
end
|
||||
@@ -805,7 +764,10 @@ function M.buffers() -- {{{
|
||||
-- trim leading `cwd` from the buffer name:
|
||||
if item_name:sub(1, #cwd) == cwd then item_name = item_name:sub(#cwd + 1) end
|
||||
|
||||
return TreeBuilder.new():put(item.changed == 1 and '[+] ' or ' '):put(item_name):tree()
|
||||
return TreeBuilder.new()
|
||||
:put(item.changed == 1 and '[+] ' or ' ')
|
||||
:put(item_name)
|
||||
:tree()
|
||||
end,
|
||||
|
||||
--- @params items { bufnr: number }[]
|
||||
@@ -918,9 +880,7 @@ function M.lsp_code_symbols() -- {{{
|
||||
local item = items[1]
|
||||
|
||||
-- Jump to the file/buffer:
|
||||
local buf = vim
|
||||
.iter(vim.fn.getbufinfo { buflisted = 1 })
|
||||
:find(function(b) return b.name == item.filename end)
|
||||
local buf = vim.iter(vim.fn.getbufinfo { buflisted = 1 }):find(function(b) return b.name == item.filename end)
|
||||
if buf ~= nil then
|
||||
vim.api.nvim_win_set_buf(0, buf.bufnr)
|
||||
else
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
local vim_repeat = require 'u.repeat'
|
||||
local Pos = require 'u.pos'
|
||||
local Range = require 'u.range'
|
||||
local Buffer = require 'u.buffer'
|
||||
local CodeWriter = require 'u.codewriter'
|
||||
|
||||
local M = {}
|
||||
|
||||
local ESC = vim.api.nvim_replace_termcodes('<Esc>', true, false, true)
|
||||
|
||||
local surrounds = {
|
||||
[')'] = { left = '(', right = ')' },
|
||||
['('] = { left = '( ', right = ' )' },
|
||||
@@ -127,7 +126,7 @@ function M.setup()
|
||||
|
||||
do_surround(range, bounds)
|
||||
-- this is a visual mapping: end in normal mode:
|
||||
vim.cmd.normal(ESC)
|
||||
vim.cmd { cmd = 'normal', args = { '' }, bang = true }
|
||||
end, { noremap = true, silent = true })
|
||||
|
||||
-- Change
|
||||
@@ -170,19 +169,29 @@ function M.setup()
|
||||
local irange = Range.from_motion('i' .. from_c, { user_defined = true })
|
||||
if arange == nil or irange == nil then return end
|
||||
|
||||
local lrange, rrange = arange:difference(irange)
|
||||
if not lrange or not rrange then return end
|
||||
local lrange = Range.new(arange.start, irange.start:must_next(-1))
|
||||
local rrange = Range.new(irange.stop:must_next(1), arange.stop)
|
||||
|
||||
rrange:replace(to.right)
|
||||
lrange:replace(to.left)
|
||||
else
|
||||
-- replace `from.right` with `to.right`:
|
||||
local right_text = arange:sub(-1, -#from.right)
|
||||
right_text:replace(to.right)
|
||||
local last_line = arange:line(-1):text()
|
||||
local from_right_match = last_line:match(vim.pesc(from.right) .. '$')
|
||||
if from_right_match then
|
||||
local match_start = arange.stop:clone()
|
||||
match_start.col = match_start.col - #from_right_match + 1
|
||||
Range.new(match_start, arange.stop):replace(to.right)
|
||||
end
|
||||
|
||||
-- replace `from.left` with `to.left`:
|
||||
local left_text = arange:sub(1, #from.left)
|
||||
left_text:replace(to.left)
|
||||
local first_line = arange:line(1):text()
|
||||
local from_left_match = first_line:match('^' .. vim.pesc(from.left))
|
||||
if from_left_match then
|
||||
local match_end = arange.start:clone()
|
||||
match_end.col = match_end.col + #from_left_match - 1
|
||||
Range.new(arange.start, match_end):replace(to.left)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end, { noremap = true, silent = true })
|
||||
|
||||
@@ -3,23 +3,19 @@ local Renderer = require('u.renderer').Renderer
|
||||
|
||||
--- @class u.Buffer
|
||||
--- @field bufnr number
|
||||
--- @field b vim.var_accessor
|
||||
--- @field bo vim.bo
|
||||
--- @field private renderer u.Renderer
|
||||
local Buffer = {}
|
||||
Buffer.__index = Buffer
|
||||
|
||||
--- @param bufnr? number
|
||||
--- @return u.Buffer
|
||||
function Buffer.from_nr(bufnr)
|
||||
if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
|
||||
|
||||
local renderer = Renderer.new(bufnr)
|
||||
return setmetatable({
|
||||
bufnr = bufnr,
|
||||
b = vim.b[bufnr],
|
||||
bo = vim.bo[bufnr],
|
||||
renderer = renderer,
|
||||
}, Buffer)
|
||||
}, { __index = Buffer })
|
||||
end
|
||||
|
||||
--- @return u.Buffer
|
||||
@@ -33,11 +29,25 @@ function Buffer.create(listed, scratch)
|
||||
end
|
||||
|
||||
function Buffer:set_tmp_options()
|
||||
self.bo.bufhidden = 'delete'
|
||||
self.bo.buflisted = false
|
||||
self.bo.buftype = 'nowrite'
|
||||
self:set_option('bufhidden', 'delete')
|
||||
self:set_option('buflisted', false)
|
||||
self:set_option('buftype', 'nowrite')
|
||||
end
|
||||
|
||||
--- @param nm string
|
||||
function Buffer:get_option(nm) return vim.api.nvim_get_option_value(nm, { buf = self.bufnr }) end
|
||||
|
||||
--- @param nm string
|
||||
function Buffer:set_option(nm, val)
|
||||
return vim.api.nvim_set_option_value(nm, val, { buf = self.bufnr })
|
||||
end
|
||||
|
||||
--- @param nm string
|
||||
function Buffer:get_var(nm) return vim.api.nvim_buf_get_var(self.bufnr, nm) end
|
||||
|
||||
--- @param nm string
|
||||
function Buffer:set_var(nm, val) return vim.api.nvim_buf_set_var(self.bufnr, nm, val) end
|
||||
|
||||
function Buffer:line_count() return vim.api.nvim_buf_line_count(self.bufnr) end
|
||||
|
||||
function Buffer:all() return Range.from_buf_text(self.bufnr) end
|
||||
@@ -75,54 +85,7 @@ function Buffer:autocmd(event, opts)
|
||||
vim.api.nvim_create_autocmd(event, vim.tbl_extend('force', opts, { buffer = self.bufnr }))
|
||||
end
|
||||
|
||||
--- @param tree u.renderer.Tree
|
||||
--- @param tree Tree
|
||||
function Buffer:render(tree) return self.renderer:render(tree) end
|
||||
|
||||
--- Filter buffer content through an external command (like Vim's :%!)
|
||||
--- @param cmd string[] Command to run (with arguments)
|
||||
--- @param opts? {cwd?: string, preserve_cursor?: boolean}
|
||||
--- @return nil
|
||||
--- @throws string Error message if command fails
|
||||
--- @note Special placeholders in cmd:
|
||||
--- - $FILE: replaced with the buffer's filename (if any)
|
||||
--- - $DIR: replaced with the buffer's directory (if any)
|
||||
function Buffer:filter_cmd(cmd, opts)
|
||||
opts = opts or {}
|
||||
local cwd = opts.cwd or vim.uv.cwd()
|
||||
local old_lines = self:all():lines()
|
||||
-- Save cursor position if needed, defaulting to true
|
||||
local save_pos = opts.preserve_cursor ~= false and vim.fn.winsaveview()
|
||||
|
||||
-- Run the command
|
||||
local result = vim
|
||||
.system(
|
||||
-- Replace special placeholders in `cmd` with their values:
|
||||
vim
|
||||
.iter(cmd)
|
||||
:map(function(x)
|
||||
if x == '$FILE' then return vim.api.nvim_buf_get_name(self.bufnr) end
|
||||
if x == '$DIR' then return vim.fs.dirname(vim.api.nvim_buf_get_name(self.bufnr)) end
|
||||
return x
|
||||
end)
|
||||
:totable(),
|
||||
{
|
||||
cwd = cwd,
|
||||
stdin = old_lines,
|
||||
text = true,
|
||||
}
|
||||
)
|
||||
:wait()
|
||||
|
||||
-- Check for command failure
|
||||
if result.code ~= 0 then error('Command failed: ' .. (result.stderr or '')) end
|
||||
|
||||
-- Process and apply the result
|
||||
local new_lines = vim.split(result.stdout, '\n')
|
||||
if new_lines[#new_lines] == '' then table.remove(new_lines) end
|
||||
Renderer.patch_lines(self.bufnr, old_lines, new_lines)
|
||||
|
||||
-- Restore cursor position if saved
|
||||
if save_pos then vim.fn.winrestview(save_pos) end
|
||||
end
|
||||
|
||||
return Buffer
|
||||
|
||||
@@ -5,7 +5,6 @@ local Buffer = require 'u.buffer'
|
||||
--- @field indent_level number
|
||||
--- @field indent_str string
|
||||
local CodeWriter = {}
|
||||
CodeWriter.__index = CodeWriter
|
||||
|
||||
--- @param indent_level? number
|
||||
--- @param indent_str? string
|
||||
@@ -19,7 +18,7 @@ function CodeWriter.new(indent_level, indent_str)
|
||||
indent_level = indent_level,
|
||||
indent_str = indent_str,
|
||||
}
|
||||
setmetatable(cw, CodeWriter)
|
||||
setmetatable(cw, { __index = CodeWriter })
|
||||
return cw
|
||||
end
|
||||
|
||||
@@ -38,8 +37,7 @@ function CodeWriter.from_line(line, bufnr)
|
||||
local expandtab = vim.api.nvim_get_option_value('expandtab', { buf = bufnr })
|
||||
local shiftwidth = vim.api.nvim_get_option_value('shiftwidth', { buf = bufnr })
|
||||
|
||||
--- @type number
|
||||
local indent_level
|
||||
local indent_level = 0
|
||||
local indent_str = ''
|
||||
if expandtab then
|
||||
while #indent_str < shiftwidth do
|
||||
|
||||
@@ -8,7 +8,7 @@ local __U__OpKeymapOpFunc_rhs = nil
|
||||
--- @type nil|fun(range: u.Range): fun():any|nil
|
||||
--- @param ty 'line'|'char'|'block'
|
||||
-- selene: allow(unused_variable)
|
||||
function _G.__U__OpKeymapOpFunc(ty)
|
||||
function __U__OpKeymapOpFunc(ty)
|
||||
if __U__OpKeymapOpFunc_rhs ~= nil then
|
||||
local range = Range.from_op_func(ty)
|
||||
__U__OpKeymapOpFunc_rhs(range)
|
||||
|
||||
@@ -12,17 +12,8 @@ end
|
||||
--- @field col number 1-based column index
|
||||
--- @field off number
|
||||
local Pos = {}
|
||||
Pos.__index = Pos
|
||||
Pos.MAX_COL = MAX_COL
|
||||
|
||||
function Pos.__tostring(self)
|
||||
if self.off ~= 0 then
|
||||
return string.format('Pos(%d:%d){bufnr=%d, off=%d}', self.lnum, self.col, self.bufnr, self.off)
|
||||
else
|
||||
return string.format('Pos(%d:%d){bufnr=%d}', self.lnum, self.col, self.bufnr)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param bufnr? number
|
||||
--- @param lnum number 1-based
|
||||
--- @param col number 1-based
|
||||
@@ -37,33 +28,49 @@ function Pos.new(bufnr, lnum, col, off)
|
||||
col = col,
|
||||
off = off,
|
||||
}
|
||||
setmetatable(pos, Pos)
|
||||
|
||||
local function str()
|
||||
if pos.off ~= 0 then
|
||||
return string.format('Pos(%d:%d){bufnr=%d, off=%d}', pos.lnum, pos.col, pos.bufnr, pos.off)
|
||||
else
|
||||
return string.format('Pos(%d:%d){bufnr=%d}', pos.lnum, pos.col, pos.bufnr)
|
||||
end
|
||||
end
|
||||
setmetatable(pos, {
|
||||
__index = Pos,
|
||||
__tostring = str,
|
||||
__lt = Pos.__lt,
|
||||
__le = Pos.__le,
|
||||
__eq = Pos.__eq,
|
||||
})
|
||||
return pos
|
||||
end
|
||||
|
||||
function Pos.invalid() return Pos.new(0, 0, 0, 0) end
|
||||
|
||||
function Pos.is(x)
|
||||
if not type(x) == 'table' then return false end
|
||||
local mt = getmetatable(x)
|
||||
return mt and mt.__index == Pos
|
||||
end
|
||||
|
||||
function Pos.__lt(a, b) return a.lnum < b.lnum or (a.lnum == b.lnum and a.col < b.col) end
|
||||
function Pos.__le(a, b) return a < b or a == b end
|
||||
function Pos.__eq(a, b)
|
||||
return getmetatable(a) == Pos
|
||||
and getmetatable(b) == Pos
|
||||
and a.bufnr == b.bufnr
|
||||
and a.lnum == b.lnum
|
||||
and a.col == b.col
|
||||
return Pos.is(a) and Pos.is(b) and a.bufnr == b.bufnr and a.lnum == b.lnum and a.col == b.col
|
||||
end
|
||||
function Pos.__add(x, y)
|
||||
if type(x) == 'number' then
|
||||
x, y = y, x
|
||||
end
|
||||
if getmetatable(x) ~= Pos or type(y) ~= 'number' then return nil end
|
||||
if not Pos.is(x) or type(y) ~= 'number' then return nil end
|
||||
return x:next(y)
|
||||
end
|
||||
function Pos.__sub(x, y)
|
||||
if type(x) == 'number' then
|
||||
x, y = y, x
|
||||
end
|
||||
if getmetatable(x) ~= Pos or type(y) ~= 'number' then return nil end
|
||||
if not Pos.is(x) or type(y) ~= 'number' then return nil end
|
||||
return x:next(-y)
|
||||
end
|
||||
|
||||
|
||||
340
lua/u/range.lua
340
lua/u/range.lua
@@ -1,39 +1,17 @@
|
||||
local Pos = require 'u.pos'
|
||||
|
||||
local ESC = vim.api.nvim_replace_termcodes('<Esc>', true, false, true)
|
||||
local orig_on_yank = (vim.hl or vim.highlight).on_yank
|
||||
local on_yank_enabled = true;
|
||||
((vim.hl or vim.highlight) --[[@as any]]).on_yank = function(opts)
|
||||
if not on_yank_enabled then return end
|
||||
return orig_on_yank(opts)
|
||||
end
|
||||
|
||||
--- @class u.Range
|
||||
--- @field start u.Pos
|
||||
--- @field stop u.Pos|nil
|
||||
--- @field mode 'v'|'V'
|
||||
local Range = {}
|
||||
Range.__index = Range
|
||||
function Range.__tostring(self)
|
||||
--- @param p u.Pos
|
||||
local function posstr(p)
|
||||
if p == nil then
|
||||
return 'nil'
|
||||
elseif p.off ~= 0 then
|
||||
return string.format('Pos(%d:%d){off=%d}', p.lnum, p.col, p.off)
|
||||
else
|
||||
return string.format('Pos(%d:%d)', p.lnum, p.col)
|
||||
end
|
||||
end
|
||||
|
||||
local _1 = posstr(self.start)
|
||||
local _2 = posstr(self.stop)
|
||||
return string.format(
|
||||
'Range{bufnr=%d, mode=%s, start=%s, stop=%s}',
|
||||
self.start.bufnr,
|
||||
self.mode,
|
||||
_1,
|
||||
_2
|
||||
)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Range constructors:
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
--- @param start u.Pos
|
||||
--- @param stop u.Pos|nil
|
||||
@@ -45,24 +23,35 @@ function Range.new(start, stop, mode)
|
||||
end
|
||||
|
||||
local r = { start = start, stop = stop, mode = mode or 'v' }
|
||||
local function str()
|
||||
--- @param p u.Pos
|
||||
local function posstr(p)
|
||||
if p == nil then
|
||||
return 'nil'
|
||||
elseif p.off ~= 0 then
|
||||
return string.format('Pos(%d:%d){off=%d}', p.lnum, p.col, p.off)
|
||||
else
|
||||
return string.format('Pos(%d:%d)', p.lnum, p.col)
|
||||
end
|
||||
end
|
||||
|
||||
setmetatable(r, Range)
|
||||
local _1 = posstr(r.start)
|
||||
local _2 = posstr(r.stop)
|
||||
return string.format(
|
||||
'Range{bufnr=%d, mode=%s, start=%s, stop=%s}',
|
||||
r.start.bufnr,
|
||||
r.mode,
|
||||
_1,
|
||||
_2
|
||||
)
|
||||
end
|
||||
setmetatable(r, { __index = Range, __tostring = str })
|
||||
return r
|
||||
end
|
||||
|
||||
--- @param ranges (u.Range|nil)[]
|
||||
function Range.smallest(ranges)
|
||||
--- @type u.Range[]
|
||||
ranges = vim.iter(ranges):filter(function(r) return r ~= nil and not r:is_empty() end):totable()
|
||||
if #ranges == 0 then return nil end
|
||||
|
||||
-- find smallest match
|
||||
local smallest = ranges[1]
|
||||
for _, r in ipairs(ranges) do
|
||||
local start, stop = r.start, r.stop
|
||||
if start > smallest.start and stop < smallest.stop then smallest = r end
|
||||
end
|
||||
return smallest
|
||||
function Range.is(x)
|
||||
local mt = getmetatable(x)
|
||||
return mt and mt.__index == Range
|
||||
end
|
||||
|
||||
--- @param lpos string
|
||||
@@ -125,6 +114,10 @@ function Range.from_motion(motion, opts)
|
||||
local is_txtobj = scope == 'a' or scope == 'i'
|
||||
local is_quote_txtobj = is_txtobj and vim.tbl_contains({ "'", '"', '`' }, motion_rest)
|
||||
|
||||
--- @type u.Pos
|
||||
local start
|
||||
--- @type u.Pos
|
||||
local stop
|
||||
-- Capture the original state of the buffer for restoration later.
|
||||
local original_state = {
|
||||
winview = vim.fn.winsaveview(),
|
||||
@@ -132,65 +125,59 @@ function Range.from_motion(motion, opts)
|
||||
cursor = vim.fn.getpos '.',
|
||||
pos_lbrack = vim.fn.getpos "'[",
|
||||
pos_rbrack = vim.fn.getpos "']",
|
||||
opfunc = vim.go.operatorfunc,
|
||||
prev_captured_range = _G.Range__from_motion_opfunc_captured_range,
|
||||
prev_mode = vim.fn.mode(),
|
||||
vinf = Range.from_vtext(),
|
||||
}
|
||||
--- @type u.Range|nil
|
||||
_G.Range__from_motion_opfunc_captured_range = nil
|
||||
|
||||
vim.api.nvim_buf_call(opts.bufnr, function()
|
||||
if opts.pos ~= nil then opts.pos:save_to_pos '.' end
|
||||
|
||||
_G.Range__from_motion_opfunc = function(ty)
|
||||
_G.Range__from_motion_opfunc_captured_range = Range.from_op_func(ty)
|
||||
end
|
||||
vim.go.operatorfunc = 'v:lua.Range__from_motion_opfunc'
|
||||
Pos.invalid():save_to_pos "'["
|
||||
Pos.invalid():save_to_pos "']"
|
||||
|
||||
local prev_on_yank_enabled = on_yank_enabled
|
||||
on_yank_enabled = false
|
||||
vim.cmd {
|
||||
cmd = 'normal',
|
||||
bang = not opts.user_defined,
|
||||
args = { ESC .. 'g@' .. motion },
|
||||
args = { '""y' .. motion },
|
||||
mods = { silent = true },
|
||||
}
|
||||
end)
|
||||
local captured_range = _G.Range__from_motion_opfunc_captured_range
|
||||
on_yank_enabled = prev_on_yank_enabled
|
||||
|
||||
start = Pos.from_pos "'["
|
||||
stop = Pos.from_pos "']"
|
||||
end)
|
||||
-- Restore original state:
|
||||
vim.fn.winrestview(original_state.winview)
|
||||
vim.fn.setreg('"', original_state.regquote)
|
||||
vim.fn.setpos('.', original_state.cursor)
|
||||
vim.fn.setpos("'[", original_state.pos_lbrack)
|
||||
vim.fn.setpos("']", original_state.pos_rbrack)
|
||||
if original_state.prev_mode ~= 'n' then original_state.vinf:set_visual_selection() end
|
||||
vim.go.operatorfunc = original_state.opfunc
|
||||
_G.Range__from_motion_opfunc_captured_range = original_state.prev_captured_range
|
||||
|
||||
if not captured_range then return nil end
|
||||
if start == stop and start:is_invalid() then return nil end
|
||||
|
||||
-- Fixup the bounds:
|
||||
if
|
||||
-- I have no idea why, but when yanking `i"`, the stop-mark is
|
||||
-- placed on the ending quote. For other text-objects, the stop-
|
||||
-- mark is placed before the closing character.
|
||||
(is_quote_txtobj and scope == 'i' and captured_range.stop:char() == motion_rest)
|
||||
(is_quote_txtobj and scope == 'i' and stop:char() == motion_rest)
|
||||
-- *Sigh*, this also sometimes happens for `it` as well.
|
||||
or (motion == 'it' and captured_range.stop:char() == '<')
|
||||
or (motion == 'it' and stop:char() == '<')
|
||||
then
|
||||
captured_range.stop = captured_range.stop:next(-1) or captured_range.stop
|
||||
stop = stop:next(-1) or stop
|
||||
end
|
||||
if is_quote_txtobj and scope == 'a' then
|
||||
captured_range.start = captured_range.start:find_next(1, motion_rest) or captured_range.start
|
||||
captured_range.stop = captured_range.stop:find_next(-1, motion_rest) or captured_range.stop
|
||||
start = start:find_next(1, motion_rest) or start
|
||||
stop = stop:find_next(-1, motion_rest) or stop
|
||||
end
|
||||
|
||||
if
|
||||
opts.contains_cursor and not captured_range:contains(Pos.new(unpack(original_state.cursor)))
|
||||
opts.contains_cursor
|
||||
and not Range.new(start, stop):contains(Pos.new(unpack(original_state.cursor)))
|
||||
then
|
||||
return nil
|
||||
end
|
||||
|
||||
return captured_range
|
||||
return Range.new(start, stop)
|
||||
end
|
||||
|
||||
--- Get range information from the currently selected visual text.
|
||||
@@ -235,6 +222,7 @@ function Range.from_cmd_args(args)
|
||||
return Range.new(start, stop, mode)
|
||||
end
|
||||
|
||||
---
|
||||
function Range.find_nearest_brackets()
|
||||
return Range.smallest {
|
||||
Range.from_motion('a<', { contains_cursor = true }),
|
||||
@@ -252,15 +240,28 @@ function Range.find_nearest_quotes()
|
||||
}
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Structural utilities:
|
||||
--------------------------------------------------------------------------------
|
||||
--- @param ranges (u.Range|nil)[]
|
||||
function Range.smallest(ranges)
|
||||
--- @type u.Range[]
|
||||
ranges = vim.iter(ranges):filter(function(r) return r ~= nil and not r:is_empty() end):totable()
|
||||
if #ranges == 0 then return nil end
|
||||
|
||||
-- find smallest match
|
||||
local smallest = ranges[1]
|
||||
for _, r in ipairs(ranges) do
|
||||
local start, stop = r.start, r.stop
|
||||
if start > smallest.start and stop < smallest.stop then smallest = r end
|
||||
end
|
||||
return smallest
|
||||
end
|
||||
|
||||
function Range:clone()
|
||||
return Range.new(self.start:clone(), self.stop ~= nil and self.stop:clone() or nil, self.mode)
|
||||
end
|
||||
|
||||
function Range:is_empty() return self.stop == nil end
|
||||
function Range:line_count()
|
||||
if self:is_empty() then return 0 end
|
||||
return self.stop.lnum - self.start.lnum + 1
|
||||
end
|
||||
|
||||
function Range:to_linewise()
|
||||
local r = self:clone()
|
||||
@@ -272,111 +273,7 @@ function Range:to_linewise()
|
||||
return r
|
||||
end
|
||||
|
||||
--- @param x u.Pos | u.Range
|
||||
function Range:contains(x)
|
||||
if getmetatable(x) == Pos then
|
||||
return not self:is_empty() and x >= self.start and x <= self.stop
|
||||
elseif getmetatable(x) == Range then
|
||||
return self:contains(x.start) and self:contains(x.stop)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--- @param other u.Range
|
||||
--- @return u.Range|nil, u.Range|nil
|
||||
function Range:difference(other)
|
||||
local outer, inner = self, other
|
||||
if not outer:contains(inner) then
|
||||
outer, inner = inner, outer
|
||||
end
|
||||
if not outer:contains(inner) then return nil, nil end
|
||||
|
||||
local left
|
||||
if outer.start ~= inner.start then
|
||||
local stop = inner.start:clone() - 1
|
||||
left = Range.new(outer.start, stop)
|
||||
else
|
||||
left = Range.new(outer.start) -- empty range
|
||||
end
|
||||
|
||||
local right
|
||||
if inner.stop ~= outer.stop then
|
||||
local start = inner.stop:clone() + 1
|
||||
right = Range.new(start, outer.stop)
|
||||
else
|
||||
right = Range.new(inner.stop) -- empty range
|
||||
end
|
||||
|
||||
return left, right
|
||||
end
|
||||
|
||||
--- @param left string
|
||||
--- @param right string
|
||||
function Range:save_to_pos(left, right)
|
||||
if self:is_empty() then
|
||||
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
|
||||
|
||||
--- @param left string
|
||||
--- @param right string
|
||||
function Range:save_to_marks(left, right)
|
||||
if self:is_empty() then
|
||||
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
|
||||
|
||||
function Range:set_visual_selection()
|
||||
if self:is_empty() then return end
|
||||
if vim.api.nvim_get_current_buf() ~= self.start.bufnr then
|
||||
error 'Range:set_visual_selection() called on a buffer other than the current buffer'
|
||||
end
|
||||
|
||||
local curr_mode = vim.fn.mode()
|
||||
if curr_mode ~= self.mode then vim.cmd.normal { args = { self.mode }, bang = true } end
|
||||
|
||||
self.start:save_to_pos '.'
|
||||
vim.cmd.normal { args = { 'o' }, bang = true }
|
||||
self.stop:save_to_pos '.'
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Range.from_* functions:
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Text access/manipulation utilities:
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
function Range:length()
|
||||
if self:is_empty() then return 0 end
|
||||
|
||||
local line_positions =
|
||||
vim.fn.getregionpos(self.start:as_real():as_vim(), self.stop:as_real():as_vim(), { type = 'v' })
|
||||
|
||||
local len = 0
|
||||
for linenr, line in ipairs(line_positions) do
|
||||
if linenr > 1 then len = len + 1 end -- each newline is counted as a char
|
||||
local line_start_col = line[1][3]
|
||||
local line_stop_col = line[2][3]
|
||||
local line_len = line_stop_col - line_start_col + 1
|
||||
len = len + line_len
|
||||
end
|
||||
return len
|
||||
end
|
||||
|
||||
function Range:line_count()
|
||||
if self:is_empty() then return 0 end
|
||||
return self.stop.lnum - self.start.lnum + 1
|
||||
end
|
||||
function Range:is_empty() return self.stop == nil end
|
||||
|
||||
function Range:trim_start()
|
||||
if self:is_empty() then return end
|
||||
@@ -402,46 +299,8 @@ function Range:trim_stop()
|
||||
return r
|
||||
end
|
||||
|
||||
--- @param i number 1-based
|
||||
--- @param j? number 1-based
|
||||
function Range:sub(i, j)
|
||||
local line_positions =
|
||||
vim.fn.getregionpos(self.start:as_real():as_vim(), self.stop:as_real():as_vim(), { type = 'v' })
|
||||
|
||||
--- @param idx number 1-based
|
||||
--- @return u.Pos|nil
|
||||
local function get_pos(idx)
|
||||
if idx < 0 then return get_pos(self:length() + idx + 1) end
|
||||
|
||||
-- find the position of the first line that contains the i-th character:
|
||||
local curr_len = 0
|
||||
for linenr, line in ipairs(line_positions) do
|
||||
if linenr > 1 then curr_len = curr_len + 1 end -- each newline is counted as a char
|
||||
local line_start_col = line[1][3]
|
||||
local line_stop_col = line[2][3]
|
||||
local line_len = line_stop_col - line_start_col + 1
|
||||
|
||||
if curr_len + line_len >= idx then
|
||||
return Pos.new(self.start.bufnr, line[1][2], line_start_col + (idx - curr_len) - 1)
|
||||
end
|
||||
curr_len = curr_len + line_len
|
||||
end
|
||||
end
|
||||
|
||||
local start = get_pos(i)
|
||||
if not start then
|
||||
-- start is inalid, so return an empty range:
|
||||
return Range.new(self.start, nil, self.mode)
|
||||
end
|
||||
|
||||
local stop
|
||||
if j then stop = get_pos(j) end
|
||||
if not stop then
|
||||
-- stop is inalid, so return an empty range:
|
||||
return Range.new(start, nil, self.mode)
|
||||
end
|
||||
return Range.new(start, stop, 'v')
|
||||
end
|
||||
--- @param p u.Pos
|
||||
function Range:contains(p) return not self:is_empty() and p >= self.start and p <= self.stop end
|
||||
|
||||
--- @return string[]
|
||||
function Range:lines()
|
||||
@@ -452,8 +311,11 @@ end
|
||||
--- @return string
|
||||
function Range:text() return vim.fn.join(self:lines(), '\n') end
|
||||
|
||||
--- @param i number 1-based
|
||||
--- @param j? number 1-based
|
||||
function Range:sub(i, j) return self:text():sub(i, j) end
|
||||
|
||||
--- @param l number
|
||||
-- luacheck: ignore
|
||||
--- @return { line: string; idx0: { start: number; stop: number; }; lnum: number; range: fun():u.Range; text: fun():string }|nil
|
||||
function Range:line(l)
|
||||
if l < 0 then l = self:line_count() + l + 1 end
|
||||
@@ -557,6 +419,44 @@ function Range:must_shrink(amount)
|
||||
return shrunk
|
||||
end
|
||||
|
||||
--- @param left string
|
||||
--- @param right string
|
||||
function Range:save_to_pos(left, right)
|
||||
if self:is_empty() then
|
||||
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
|
||||
|
||||
--- @param left string
|
||||
--- @param right string
|
||||
function Range:save_to_marks(left, right)
|
||||
if self:is_empty() then
|
||||
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
|
||||
|
||||
function Range:set_visual_selection()
|
||||
if self:is_empty() then return end
|
||||
if vim.api.nvim_get_current_buf() ~= self.start.bufnr then
|
||||
error 'Range:set_visual_selection() called on a buffer other than the current buffer'
|
||||
end
|
||||
|
||||
local curr_mode = vim.fn.mode()
|
||||
if curr_mode ~= self.mode then vim.cmd.normal { args = { self.mode }, bang = true } end
|
||||
|
||||
self.start:save_to_pos '.'
|
||||
vim.cmd.normal { args = { 'o' }, bang = true }
|
||||
self.stop:save_to_pos '.'
|
||||
end
|
||||
|
||||
--- @param group string
|
||||
--- @param opts? { timeout?: number, priority?: number, on_macro?: boolean }
|
||||
function Range:highlight(group, opts)
|
||||
@@ -570,8 +470,8 @@ function Range:highlight(group, opts)
|
||||
|
||||
local ns = vim.api.nvim_create_namespace ''
|
||||
|
||||
local winview = vim.fn.winsaveview()
|
||||
vim.hl.range(
|
||||
local winview = vim.fn.winsaveview();
|
||||
(vim.hl or vim.highlight).range(
|
||||
self.start.bufnr,
|
||||
ns,
|
||||
group,
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
local M = {}
|
||||
local H = {}
|
||||
|
||||
--- @alias u.renderer.Tag { kind: 'tag'; name: string, attributes: table<string, unknown>, children: u.renderer.Tree }
|
||||
--- @alias u.renderer.Node nil | boolean | string | u.renderer.Tag
|
||||
--- @alias u.renderer.Tree u.renderer.Node | u.renderer.Node[]
|
||||
--- @alias Tag { kind: 'tag'; name: string, attributes: table<string, unknown>, children: Tree }
|
||||
--- @alias Node nil | boolean | string | Tag
|
||||
--- @alias Tree Node | Node[]
|
||||
|
||||
-- luacheck: ignore
|
||||
--- @type table<string, fun(attributes: table<string, any>, children: u.renderer.Tree): u.renderer.Tag> & fun(name: string, attributes: table<string, any>, children: u.renderer.Tree): u.renderer.Tag>
|
||||
M.h = setmetatable({}, {
|
||||
__call = function(_, name, attributes, children)
|
||||
--- @param name string
|
||||
--- @param attributes? table<string, any>
|
||||
--- @param children? Node | Node[]
|
||||
--- @return Tag
|
||||
function M.h(name, attributes, children)
|
||||
return {
|
||||
kind = 'tag',
|
||||
name = name,
|
||||
attributes = attributes or {},
|
||||
children = children,
|
||||
}
|
||||
end,
|
||||
__index = function(_, name)
|
||||
-- vim.print('dynamic hl ' .. name)
|
||||
return function(attributes, children)
|
||||
return M.h('text', vim.tbl_deep_extend('force', { hl = name }, attributes), children)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
-- Renderer {{{
|
||||
--- @alias RendererExtmark { id?: number; start: [number, number]; stop: [number, number]; opts: any; tag: any }
|
||||
@@ -68,8 +62,8 @@ function Renderer.new(bufnr) -- {{{
|
||||
end -- }}}
|
||||
|
||||
--- @param opts {
|
||||
--- tree: u.renderer.Tree;
|
||||
--- on_tag?: fun(tag: u.renderer.Tag, start0: [number, number], stop0: [number, number]): any;
|
||||
--- tree: Tree;
|
||||
--- on_tag?: fun(tag: Tag, start0: [number, number], stop0: [number, number]): any;
|
||||
--- }
|
||||
function Renderer.markup_to_lines(opts) -- {{{
|
||||
--- @type string[]
|
||||
@@ -88,7 +82,7 @@ function Renderer.markup_to_lines(opts) -- {{{
|
||||
curr_col1 = 1
|
||||
end
|
||||
|
||||
--- @param node u.renderer.Node
|
||||
--- @param node Node
|
||||
local function visit(node) -- {{{
|
||||
if node == nil or type(node) == 'boolean' then return end
|
||||
|
||||
@@ -104,12 +98,12 @@ function Renderer.markup_to_lines(opts) -- {{{
|
||||
-- visit the children:
|
||||
if Renderer.is_tag_arr(node.children) then
|
||||
for _, child in
|
||||
ipairs(node.children --[[@as u.renderer.Node[] ]])
|
||||
ipairs(node.children --[[@as Node[] ]])
|
||||
do
|
||||
-- newlines are not controlled by array entries, do NOT output a line here:
|
||||
visit(child)
|
||||
end
|
||||
else -- luacheck: ignore
|
||||
else
|
||||
visit(node.children)
|
||||
end
|
||||
|
||||
@@ -129,69 +123,11 @@ end -- }}}
|
||||
|
||||
--- @param opts {
|
||||
--- tree: string;
|
||||
--- format_tag?: fun(tag: u.renderer.Tag): string;
|
||||
--- format_tag?: fun(tag: Tag): string;
|
||||
--- }
|
||||
function Renderer.markup_to_string(opts) return table.concat(Renderer.markup_to_lines(opts), '\n') end
|
||||
|
||||
--- @param bufnr number
|
||||
--- @param old_lines string[] | nil
|
||||
--- @param new_lines string[]
|
||||
function Renderer.patch_lines(bufnr, old_lines, new_lines)
|
||||
--
|
||||
-- Helpers:
|
||||
--
|
||||
|
||||
--- @param start integer
|
||||
--- @param end_ integer
|
||||
--- @param strict_indexing boolean
|
||||
--- @param replacement string[]
|
||||
local function _set_lines(start, end_, strict_indexing, replacement)
|
||||
vim.api.nvim_buf_set_lines(bufnr, start, end_, strict_indexing, replacement)
|
||||
end
|
||||
|
||||
--- @param start_row integer
|
||||
--- @param start_col integer
|
||||
--- @param end_row integer
|
||||
--- @param end_col integer
|
||||
--- @param replacement string[]
|
||||
local function _set_text(start_row, start_col, end_row, end_col, replacement)
|
||||
vim.api.nvim_buf_set_text(bufnr, start_row, start_col, end_row, end_col, replacement)
|
||||
end
|
||||
|
||||
-- Morph the text to the desired state:
|
||||
local line_changes =
|
||||
H.levenshtein(old_lines or vim.api.nvim_buf_get_lines(bufnr, 0, -1, false), new_lines)
|
||||
for _, line_change in ipairs(line_changes) do
|
||||
local lnum0 = line_change.index - 1
|
||||
|
||||
if line_change.kind == 'add' then
|
||||
_set_lines(lnum0, lnum0, true, { line_change.item })
|
||||
elseif line_change.kind == 'change' then
|
||||
-- Compute inter-line diff, and apply:
|
||||
local col_changes =
|
||||
H.levenshtein(vim.split(line_change.from, ''), vim.split(line_change.to, ''))
|
||||
|
||||
for _, col_change in ipairs(col_changes) do
|
||||
local cnum0 = col_change.index - 1
|
||||
if col_change.kind == 'add' then
|
||||
_set_text(lnum0, cnum0, lnum0, cnum0, { col_change.item })
|
||||
elseif col_change.kind == 'change' then
|
||||
_set_text(lnum0, cnum0, lnum0, cnum0 + 1, { col_change.to })
|
||||
elseif col_change.kind == 'delete' then
|
||||
_set_text(lnum0, cnum0, lnum0, cnum0 + 1, {})
|
||||
else -- luacheck: ignore
|
||||
-- No change
|
||||
end
|
||||
end
|
||||
elseif line_change.kind == 'delete' then
|
||||
_set_lines(lnum0, lnum0 + 1, true, {})
|
||||
else -- luacheck: ignore
|
||||
-- No change
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param tree u.renderer.Tree
|
||||
--- @param tree Tree
|
||||
function Renderer:render(tree) -- {{{
|
||||
local changedtick = vim.b[self.bufnr].changedtick
|
||||
if changedtick ~= self.changedtick then
|
||||
@@ -269,16 +205,45 @@ end
|
||||
|
||||
--- @private
|
||||
function Renderer:_reconcile() -- {{{
|
||||
local line_changes = H.levenshtein(self.old.lines, self.curr.lines)
|
||||
self.old = self.curr
|
||||
|
||||
--
|
||||
-- Step 1: morph the text to the desired state:
|
||||
--
|
||||
Renderer.patch_lines(self.bufnr, self.old.lines, self.curr.lines)
|
||||
for _, line_change in ipairs(line_changes) do
|
||||
local lnum0 = line_change.index - 1
|
||||
|
||||
if line_change.kind == 'add' then
|
||||
self:_set_lines(lnum0, lnum0, true, { line_change.item })
|
||||
elseif line_change.kind == 'change' then
|
||||
-- Compute inter-line diff, and apply:
|
||||
local col_changes =
|
||||
H.levenshtein(vim.split(line_change.from, ''), vim.split(line_change.to, ''))
|
||||
|
||||
for _, col_change in ipairs(col_changes) do
|
||||
local cnum0 = col_change.index - 1
|
||||
if col_change.kind == 'add' then
|
||||
self:_set_text(lnum0, cnum0, lnum0, cnum0, { col_change.item })
|
||||
elseif col_change.kind == 'change' then
|
||||
self:_set_text(lnum0, cnum0, lnum0, cnum0 + 1, { col_change.to })
|
||||
elseif col_change.kind == 'delete' then
|
||||
self:_set_text(lnum0, cnum0, lnum0, cnum0 + 1, {})
|
||||
else
|
||||
-- No change
|
||||
end
|
||||
end
|
||||
elseif line_change.kind == 'delete' then
|
||||
self:_set_lines(lnum0, lnum0 + 1, true, {})
|
||||
else
|
||||
-- No change
|
||||
end
|
||||
end
|
||||
self.changedtick = vim.b[self.bufnr].changedtick
|
||||
|
||||
--
|
||||
-- Step 2: reconcile extmarks:
|
||||
--
|
||||
|
||||
-- Clear current extmarks:
|
||||
vim.api.nvim_buf_clear_namespace(self.bufnr, self.ns, 0, -1)
|
||||
-- Set current extmarks:
|
||||
@@ -295,8 +260,6 @@ function Renderer:_reconcile() -- {{{
|
||||
}, extmark.opts)
|
||||
)
|
||||
end
|
||||
|
||||
self.old = self.curr
|
||||
end -- }}}
|
||||
|
||||
--- @private
|
||||
@@ -339,7 +302,7 @@ end -- }}}
|
||||
---
|
||||
--- @private (private for now)
|
||||
--- @param pos0 [number; number]
|
||||
--- @return { extmark: RendererExtmark; tag: u.renderer.Tag; }[]
|
||||
--- @return { extmark: RendererExtmark; tag: Tag; }[]
|
||||
function Renderer:get_pos_infos(pos0) -- {{{
|
||||
local cursor_line0, cursor_col0 = pos0[1], pos0[2]
|
||||
|
||||
@@ -407,7 +370,7 @@ function Renderer:get_pos_infos(pos0) -- {{{
|
||||
-- created extmarks in self.curr.extmarks, which also has which tag each
|
||||
-- extmark is associated with. Cross-reference with that list to get a list
|
||||
-- of tags that we need to fire events for:
|
||||
--- @type { extmark: RendererExtmark; tag: u.renderer.Tag }[]
|
||||
--- @type { extmark: RendererExtmark; tag: Tag }[]
|
||||
local matching_tags = vim
|
||||
.iter(intersecting_extmarks)
|
||||
--- @param ext RendererExtmark
|
||||
@@ -424,7 +387,7 @@ end -- }}}
|
||||
|
||||
-- TreeBuilder {{{
|
||||
--- @class u.TreeBuilder
|
||||
--- @field private nodes u.renderer.Node[]
|
||||
--- @field private nodes Node[]
|
||||
local TreeBuilder = {}
|
||||
TreeBuilder.__index = TreeBuilder
|
||||
M.TreeBuilder = TreeBuilder
|
||||
@@ -434,7 +397,7 @@ function TreeBuilder.new()
|
||||
return self
|
||||
end
|
||||
|
||||
--- @param nodes u.renderer.Tree
|
||||
--- @param nodes Tree
|
||||
--- @return u.TreeBuilder
|
||||
function TreeBuilder:put(nodes)
|
||||
table.insert(self.nodes, nodes)
|
||||
@@ -443,7 +406,7 @@ end
|
||||
|
||||
--- @param name string
|
||||
--- @param attributes? table<string, any>
|
||||
--- @param children? u.renderer.Node | u.renderer.Node[]
|
||||
--- @param children? Node | Node[]
|
||||
--- @return u.TreeBuilder
|
||||
function TreeBuilder:put_h(name, attributes, children)
|
||||
local tag = M.h(name, attributes, children)
|
||||
@@ -460,19 +423,18 @@ function TreeBuilder:nest(fn)
|
||||
return self
|
||||
end
|
||||
|
||||
--- @return u.renderer.Tree
|
||||
--- @return Tree
|
||||
function TreeBuilder:tree() return self.nodes end
|
||||
-- }}}
|
||||
|
||||
-- Levenshtein utility {{{
|
||||
-- luacheck: ignore
|
||||
--- @alias LevenshteinChange<T> ({ kind: 'add'; item: T; index: number; } | { kind: 'delete'; item: T; index: number; } | { kind: 'change'; from: T; to: T; index: number; })
|
||||
--- @private
|
||||
--- @generic T
|
||||
--- @param x `T`[]
|
||||
--- @param y T[]
|
||||
--- @param cost? { of_delete?: fun(x: T): number; of_add?: fun(x: T): number; of_change?: fun(x: T, y: T): number; }
|
||||
--- @return LevenshteinChange<T>[] The changes, from last (greatest index) to first (smallest index).
|
||||
--- @return LevenshteinChange<T>[]
|
||||
function H.levenshtein(x, y, cost)
|
||||
-- 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
|
||||
@@ -512,10 +474,10 @@ function H.levenshtein(x, y, cost)
|
||||
if x[i] == y[j] then
|
||||
dp[i][j] = dp[i - 1][j - 1] -- no cost if items are the same
|
||||
else
|
||||
local cost_delete = dp[i - 1][j] + cost_of_delete_f(x[i])
|
||||
local cost_add = dp[i][j - 1] + cost_of_add_f(y[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)
|
||||
local costDelete = dp[i - 1][j] + cost_of_delete_f(x[i])
|
||||
local costAdd = dp[i][j - 1] + cost_of_add_f(y[j])
|
||||
local costChange = dp[i - 1][j - 1] + cost_of_change_f(x[i], y[j])
|
||||
dp[i][j] = math.min(costDelete, costAdd, costChange)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -202,7 +202,7 @@ end
|
||||
-- class ExecutionContext
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local CURRENT_CONTEXT = nil
|
||||
CURRENT_CONTEXT = nil
|
||||
|
||||
--- @class u.ExecutionContext
|
||||
--- @field signals table<u.Signal, boolean>
|
||||
@@ -211,7 +211,7 @@ M.ExecutionContext = ExecutionContext
|
||||
ExecutionContext.__index = ExecutionContext
|
||||
|
||||
--- @return u.ExecutionContext
|
||||
function ExecutionContext.new()
|
||||
function ExecutionContext:new()
|
||||
return setmetatable({
|
||||
signals = {},
|
||||
subscribers = {},
|
||||
@@ -222,7 +222,7 @@ function ExecutionContext.current() return CURRENT_CONTEXT end
|
||||
|
||||
--- @param fn function
|
||||
--- @param ctx u.ExecutionContext
|
||||
function ExecutionContext.run(fn, ctx)
|
||||
function ExecutionContext:run(fn, ctx)
|
||||
local oldCtx = CURRENT_CONTEXT
|
||||
CURRENT_CONTEXT = ctx
|
||||
local result
|
||||
@@ -289,8 +289,8 @@ end
|
||||
--- @param fn function
|
||||
--- @param name? string
|
||||
function M.create_effect(fn, name)
|
||||
local ctx = M.ExecutionContext.new()
|
||||
M.ExecutionContext.run(fn, ctx)
|
||||
local ctx = M.ExecutionContext:new()
|
||||
M.ExecutionContext:run(fn, ctx)
|
||||
return ctx:subscribe(function()
|
||||
if name and M.debug then
|
||||
local deps = vim
|
||||
|
||||
@@ -6,7 +6,6 @@ local M = {}
|
||||
|
||||
--- @alias QfItem { col: number, filename: string, kind: string, lnum: number, text: string }
|
||||
--- @alias KeyMaps table<string, fun(): any | string> }
|
||||
-- luacheck: ignore
|
||||
--- @alias CmdArgs { args: string; bang: boolean; count: number; fargs: string[]; line1: number; line2: number; mods: string; name: string; range: 0|1|2; reg: string; smods: any; info: u.Range|nil }
|
||||
|
||||
--- @generic T
|
||||
@@ -35,7 +34,6 @@ end
|
||||
--- ```
|
||||
--- @param name string
|
||||
--- @param cmd string | fun(args: CmdArgs): any
|
||||
-- luacheck: ignore
|
||||
--- @param opts? { nargs?: 0|1|'*'|'?'|'+'; range?: boolean|'%'|number; count?: boolean|number, addr?: string; completion?: string }
|
||||
function M.ucmd(name, cmd, opts)
|
||||
local Range = require 'u.range'
|
||||
|
||||
19
lux.toml
Normal file
19
lux.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
package = "u.nvim"
|
||||
version = "0.2.0"
|
||||
lua = ">=5.1"
|
||||
|
||||
[description]
|
||||
summary = ""
|
||||
maintainer = "jrop"
|
||||
labels = [ "library", "neovim", "neovim-plugin", "range", "utility" ]
|
||||
|
||||
|
||||
[dependencies]
|
||||
# Add your dependencies here
|
||||
# `busted = ">=2.0"`
|
||||
|
||||
[run]
|
||||
args = [ "src/main.lua" ]
|
||||
|
||||
[build]
|
||||
type = "builtin"
|
||||
4
selene.toml
Normal file
4
selene.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
std = "vim"
|
||||
|
||||
[lints]
|
||||
multiple_statements = "allow"
|
||||
23
shell.nix
23
shell.nix
@@ -1,23 +0,0 @@
|
||||
{
|
||||
pkgs ?
|
||||
import
|
||||
# nixpkgs-unstable (neovim@0.11.2):
|
||||
(fetchTarball {
|
||||
url = "https://github.com/nixos/nixpkgs/archive/e4b09e47ace7d87de083786b404bf232eb6c89d8.tar.gz";
|
||||
sha256 = "1a2qvp2yz8j1jcggl1yvqmdxicbdqq58nv7hihmw3bzg9cjyqm26";
|
||||
})
|
||||
{ },
|
||||
}:
|
||||
pkgs.mkShell {
|
||||
packages = [
|
||||
pkgs.git
|
||||
pkgs.gnumake
|
||||
pkgs.lua-language-server
|
||||
pkgs.lua51Packages.busted
|
||||
pkgs.lua51Packages.luacov
|
||||
pkgs.lua51Packages.luarocks
|
||||
pkgs.lua51Packages.nlua
|
||||
pkgs.neovim
|
||||
pkgs.stylua
|
||||
];
|
||||
}
|
||||
@@ -422,52 +422,6 @@ describe('Range', function()
|
||||
end)
|
||||
end)
|
||||
|
||||
it('difference', function()
|
||||
withbuf({ 'line one', 'and line two' }, function()
|
||||
local range_outer = Range.new(Pos.new(nil, 2, 1), Pos.new(nil, 2, 12), 'v')
|
||||
local range_inner = Range.new(Pos.new(nil, 2, 5), Pos.new(nil, 2, 8), 'v')
|
||||
|
||||
assert.are.same(range_outer:text(), 'and line two')
|
||||
assert.are.same(range_inner:text(), 'line')
|
||||
|
||||
local left, right = range_outer:difference(range_inner)
|
||||
assert.are.same(left:text(), 'and ')
|
||||
assert.are.same(right:text(), ' two')
|
||||
|
||||
left, right = range_inner:difference(range_outer)
|
||||
assert.are.same(left:text(), 'and ')
|
||||
assert.are.same(right:text(), ' two')
|
||||
|
||||
left, right = range_outer:difference(range_outer)
|
||||
assert.are.same(left:is_empty(), true)
|
||||
assert.are.same(left:text(), '')
|
||||
assert.are.same(right:is_empty(), true)
|
||||
assert.are.same(right:text(), '')
|
||||
end)
|
||||
end)
|
||||
|
||||
it('length', function()
|
||||
withbuf({ 'line one', 'and line two' }, function()
|
||||
local range = Range.new(Pos.new(nil, 2, 4), Pos.new(nil, 2, 9), 'v')
|
||||
assert.are.same(range:length(), #range:text())
|
||||
|
||||
range = Range.new(Pos.new(nil, 1, 4), Pos.new(nil, 2, 9), 'v')
|
||||
assert.are.same(range:length(), #range:text())
|
||||
end)
|
||||
end)
|
||||
|
||||
it('sub', function()
|
||||
withbuf({ 'line one', 'and line two' }, function()
|
||||
local range = Range.new(Pos.new(nil, 2, 4), Pos.new(nil, 2, 9), 'v')
|
||||
assert.are.same(range:text(), ' line ')
|
||||
assert.are.same(range:sub(1, -1):text(), ' line ')
|
||||
assert.are.same(range:sub(2, -2):text(), 'line')
|
||||
assert.are.same(range:sub(1, 5):text(), ' line')
|
||||
assert.are.same(range:sub(2, 5):text(), 'line')
|
||||
assert.are.same(range:sub(20, 25):text(), '')
|
||||
end)
|
||||
end)
|
||||
|
||||
it('shrink', function()
|
||||
withbuf({ 'line one', 'and line two' }, function()
|
||||
local range = Range.new(Pos.new(nil, 2, 3), Pos.new(nil, 3, 5), 'v')
|
||||
@@ -484,10 +438,7 @@ describe('Range', function()
|
||||
assert.are.same(shrunk.start, Pos.new(nil, 2, 4))
|
||||
assert.are.same(shrunk.stop, Pos.new(nil, 3, 4))
|
||||
|
||||
assert.has.error(
|
||||
function() range:must_shrink(100) end,
|
||||
'error in Range:must_shrink: Range:shrink() returned nil'
|
||||
)
|
||||
assert.has.error(function() range:must_shrink(100) end, 'error in Range:must_shrink: Range:shrink() returned nil')
|
||||
end)
|
||||
end)
|
||||
|
||||
@@ -525,16 +476,10 @@ describe('Range', function()
|
||||
local r = Range.new(Pos.new(b, 1, 5), Pos.new(b, 1, 9), 'v')
|
||||
|
||||
r:replace 'bleh1'
|
||||
assert.are.same(
|
||||
{ 'The bleh1 brown fox jumps over the lazy dog' },
|
||||
vim.api.nvim_buf_get_lines(b, 0, -1, false)
|
||||
)
|
||||
assert.are.same({ 'The bleh1 brown fox jumps over the lazy dog' }, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
||||
|
||||
r:replace 'bleh2'
|
||||
assert.are.same(
|
||||
{ 'The bleh2 brown fox jumps over the lazy dog' },
|
||||
vim.api.nvim_buf_get_lines(b, 0, -1, false)
|
||||
)
|
||||
assert.are.same({ 'The bleh2 brown fox jumps over the lazy dog' }, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
||||
end)
|
||||
end)
|
||||
|
||||
@@ -548,17 +493,11 @@ describe('Range', function()
|
||||
assert.are.same({ 'jumps', 'over' }, r:lines())
|
||||
|
||||
r:replace 'bleh1'
|
||||
assert.are.same(
|
||||
{ 'The quick brown fox bleh1 the lazy dog' },
|
||||
vim.api.nvim_buf_get_lines(b, 0, -1, false)
|
||||
)
|
||||
assert.are.same({ 'The quick brown fox bleh1 the lazy dog' }, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
||||
assert.are.same({ 'bleh1' }, r:lines())
|
||||
|
||||
r:replace 'blehGoo2'
|
||||
assert.are.same(
|
||||
{ 'The quick brown fox blehGoo2 the lazy dog' },
|
||||
vim.api.nvim_buf_get_lines(b, 0, -1, false)
|
||||
)
|
||||
assert.are.same({ 'The quick brown fox blehGoo2 the lazy dog' }, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
||||
end)
|
||||
end)
|
||||
|
||||
|
||||
@@ -117,11 +117,7 @@ describe('Renderer', function()
|
||||
r:render {
|
||||
R.h('text', { hl = 'HighlightGroup1' }, {
|
||||
'Hello',
|
||||
R.h(
|
||||
'text',
|
||||
{ hl = 'HighlightGroup2', extmark = { hl_group = 'HighlightGroup2' } },
|
||||
' World'
|
||||
),
|
||||
R.h('text', { hl = 'HighlightGroup2', extmark = { hl_group = 'HighlightGroup2' } }, ' World'),
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
@@ -58,66 +58,66 @@ describe('Signal', function()
|
||||
|
||||
describe('Signal:map', function()
|
||||
it('should transform the signal value', function()
|
||||
local test_signal = Signal:new(5)
|
||||
local mapped_signal = test_signal:map(function(value) return value * 2 end)
|
||||
local signal = Signal:new(5)
|
||||
local mapped_signal = signal:map(function(value) return value * 2 end)
|
||||
|
||||
assert.is.equal(mapped_signal:get(), 10) -- Initial transformation
|
||||
test_signal:set(10)
|
||||
signal:set(10)
|
||||
assert.is.equal(mapped_signal:get(), 20) -- Updated transformation
|
||||
end)
|
||||
|
||||
it('should handle empty transformations', function()
|
||||
local test_signal = Signal:new(nil)
|
||||
local mapped_signal = test_signal:map(function(value) return value or 'default' end)
|
||||
local signal = Signal:new(nil)
|
||||
local mapped_signal = signal:map(function(value) return value or 'default' end)
|
||||
|
||||
assert.is.equal(mapped_signal:get(), 'default') -- Return default
|
||||
test_signal:set 'new value'
|
||||
signal:set 'new value'
|
||||
assert.is.equal(mapped_signal:get(), 'new value') -- Return new value
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('Signal:filter', function()
|
||||
it('should only emit values that pass the filter', function()
|
||||
local test_signal = Signal:new(5)
|
||||
local filtered_signal = test_signal:filter(function(value) return value > 10 end)
|
||||
local signal = Signal:new(5)
|
||||
local filtered_signal = signal:filter(function(value) return value > 10 end)
|
||||
|
||||
assert.is.equal(filtered_signal:get(), nil) -- Initial value should not pass
|
||||
test_signal:set(15)
|
||||
signal:set(15)
|
||||
assert.is.equal(filtered_signal:get(), 15) -- Now filtered
|
||||
test_signal:set(8)
|
||||
signal:set(8)
|
||||
assert.is.equal(filtered_signal:get(), 15) -- Does not pass the filter
|
||||
end)
|
||||
|
||||
it('should handle empty initial values', function()
|
||||
local test_signal = Signal:new(nil)
|
||||
local filtered_signal = test_signal:filter(function(value) return value ~= nil end)
|
||||
local signal = Signal:new(nil)
|
||||
local filtered_signal = signal:filter(function(value) return value ~= nil end)
|
||||
|
||||
assert.is.equal(filtered_signal:get(), nil) -- Should be nil
|
||||
test_signal:set(10)
|
||||
signal:set(10)
|
||||
assert.is.equal(filtered_signal:get(), 10) -- Should pass now
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('create_memo', function()
|
||||
it('should compute a derived value and update when dependencies change', function()
|
||||
local test_signal = Signal:new(2)
|
||||
local memoized_signal = tracker.create_memo(function() return test_signal:get() * 2 end)
|
||||
local signal = Signal:new(2)
|
||||
local memoized_signal = tracker.create_memo(function() return signal:get() * 2 end)
|
||||
|
||||
assert.is.equal(memoized_signal:get(), 4) -- Initially compute 2 * 2
|
||||
|
||||
test_signal:set(3)
|
||||
signal:set(3)
|
||||
assert.is.equal(memoized_signal:get(), 6) -- Update to 3 * 2 = 6
|
||||
|
||||
test_signal:set(5)
|
||||
signal:set(5)
|
||||
assert.is.equal(memoized_signal:get(), 10) -- Update to 5 * 2 = 10
|
||||
end)
|
||||
|
||||
it('should not recompute if the dependencies do not change', function()
|
||||
local call_count = 0
|
||||
local test_signal = Signal:new(10)
|
||||
local signal = Signal:new(10)
|
||||
local memoized_signal = tracker.create_memo(function()
|
||||
call_count = call_count + 1
|
||||
return test_signal:get() + 1
|
||||
return signal:get() + 1
|
||||
end)
|
||||
|
||||
assert.is.equal(memoized_signal:get(), 11) -- Compute first value
|
||||
@@ -126,11 +126,11 @@ describe('Signal', function()
|
||||
memoized_signal:get() -- Call again, should use memoized value
|
||||
assert.is.equal(call_count, 1) -- Still should only be one call
|
||||
|
||||
test_signal:set(10) -- Set the same value
|
||||
signal:set(10) -- Set the same value
|
||||
assert.is.equal(memoized_signal:get(), 11)
|
||||
assert.is.equal(call_count, 2)
|
||||
|
||||
test_signal:set(20)
|
||||
signal:set(20)
|
||||
assert.is.equal(memoized_signal:get(), 21)
|
||||
assert.is.equal(call_count, 3)
|
||||
end)
|
||||
@@ -138,31 +138,31 @@ describe('Signal', function()
|
||||
|
||||
describe('create_effect', function()
|
||||
it('should track changes and execute callback', function()
|
||||
local test_signal = Signal:new(5)
|
||||
local signal = Signal:new(5)
|
||||
local call_count = 0
|
||||
|
||||
tracker.create_effect(function()
|
||||
test_signal:get() -- track as a dependency
|
||||
signal:get() -- track as a dependency
|
||||
call_count = call_count + 1
|
||||
end)
|
||||
|
||||
assert.is.equal(call_count, 1)
|
||||
test_signal:set(10)
|
||||
signal:set(10)
|
||||
assert.is.equal(call_count, 2)
|
||||
end)
|
||||
|
||||
it('should clean up signals and not call after dispose', function()
|
||||
local test_signal = Signal:new(5)
|
||||
local signal = Signal:new(5)
|
||||
local call_count = 0
|
||||
|
||||
local unsubscribe = tracker.create_effect(function()
|
||||
call_count = call_count + 1
|
||||
return test_signal:get() * 2
|
||||
return signal:get() * 2
|
||||
end)
|
||||
|
||||
assert.is.equal(call_count, 1) -- Initially calls
|
||||
unsubscribe() -- Unsubscribe the effect
|
||||
test_signal:set(10) -- Update signal value
|
||||
signal:set(10) -- Update signal value
|
||||
assert.is.equal(call_count, 1) -- Callback should not be called again
|
||||
end)
|
||||
end)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
require 'luacov'
|
||||
local function withbuf(lines, f)
|
||||
vim.go.swapfile = false
|
||||
|
||||
|
||||
36
vim.yml
Normal file
36
vim.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
base: lua51
|
||||
globals:
|
||||
vim:
|
||||
any: true
|
||||
assert.are.same:
|
||||
args:
|
||||
- type: any
|
||||
- type: any
|
||||
assert.are_not.same:
|
||||
args:
|
||||
- type: any
|
||||
- type: any
|
||||
assert.has.error:
|
||||
args:
|
||||
- type: any
|
||||
- type: any
|
||||
assert.is_true:
|
||||
args:
|
||||
- type: any
|
||||
- type: any
|
||||
assert.is_false:
|
||||
args:
|
||||
- type: any
|
||||
- type: any
|
||||
describe:
|
||||
args:
|
||||
- type: string
|
||||
- type: function
|
||||
it:
|
||||
args:
|
||||
- type: string
|
||||
- type: function
|
||||
before_each:
|
||||
args:
|
||||
- type: function
|
||||
Reference in New Issue
Block a user