Compare commits
12 Commits
87930bf3af
...
react
| Author | SHA1 | Date | |
|---|---|---|---|
| 629bdf27b4 | |||
| 88b9e5f965 | |||
| fd7b53ab05 | |||
| 9fcc803805 | |||
| 72b6886838 | |||
| 12945a4cdf | |||
| 6f86bfaa42 | |||
| 81ba1bb96b | |||
| 35b6e123ac | |||
| 237bc9ba5e | |||
| c760c495b7 | |||
| 44a97b5baa |
13
.busted
Normal file
13
.busted
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
return {
|
||||||
|
_all = {
|
||||||
|
coverage = false,
|
||||||
|
lpath = "lua/?.lua;lua/?/init.lua",
|
||||||
|
lua = "nvim -u NONE -i NONE -l",
|
||||||
|
},
|
||||||
|
default = {
|
||||||
|
verbose = true
|
||||||
|
},
|
||||||
|
tests = {
|
||||||
|
verbose = true
|
||||||
|
},
|
||||||
|
}
|
||||||
12
.emmyrc.json
Normal file
12
.emmyrc.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/EmmyLuaLs/emmylua-analyzer-rust/refs/heads/main/crates/emmylua_code_analysis/resources/schema.json",
|
||||||
|
"runtime": {
|
||||||
|
"version": "LuaJIT"
|
||||||
|
},
|
||||||
|
"workspace": {
|
||||||
|
"library": [
|
||||||
|
"$VIMRUNTIME",
|
||||||
|
"library/busted"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
25
.github/workflows/ci.yaml
vendored
25
.github/workflows/ci.yaml
vendored
@@ -2,15 +2,28 @@
|
|||||||
name: NeoVim tests
|
name: NeoVim tests
|
||||||
on: [push]
|
on: [push]
|
||||||
jobs:
|
jobs:
|
||||||
plenary-tests:
|
code-quality:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
XDG_CONFIG_HOME: ${{ github.workspace }}/.config/
|
XDG_CONFIG_HOME: ${{ github.workspace }}/.config/
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: rhysd/action-setup-vim@v1
|
- uses: cachix/install-nix-action@v31
|
||||||
with:
|
with:
|
||||||
neovim: true
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
version: v0.11.0
|
|
||||||
arch: 'x86_64'
|
- name: Populate Nix store
|
||||||
- run: make test
|
run:
|
||||||
|
nix-shell --pure --run 'true'
|
||||||
|
|
||||||
|
- name: Type-check
|
||||||
|
run:
|
||||||
|
nix-shell --pure --run 'make lint'
|
||||||
|
|
||||||
|
- name: Check formatting with stylua
|
||||||
|
run:
|
||||||
|
nix-shell --pure --run 'make fmt-check'
|
||||||
|
|
||||||
|
- name: Run busted tests
|
||||||
|
run:
|
||||||
|
nix-shell --pure --run 'make test'
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
/.lux/
|
|
||||||
*.src.rock
|
*.src.rock
|
||||||
|
*.aider*
|
||||||
|
luacov.*.out
|
||||||
|
|||||||
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[submodule "library/busted"]
|
||||||
|
path = library/busted
|
||||||
|
url = https://github.com/LuaCATS/busted
|
||||||
|
branch = main
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
-- :vim set ft=lua
|
|
||||||
globals = { "vim" }
|
|
||||||
10
.woodpecker/ci.yaml
Normal file
10
.woodpecker/ci.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
when:
|
||||||
|
- event: push
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build
|
||||||
|
image: nixos/nix
|
||||||
|
commands:
|
||||||
|
- nix-shell --pure --run 'make lint'
|
||||||
|
- nix-shell --pure --run 'make fmt-check'
|
||||||
|
- nix-shell --pure --run 'make test'
|
||||||
28
Makefile
28
Makefile
@@ -1,16 +1,26 @@
|
|||||||
PLENARY_DIR=~/.local/share/nvim/site/pack/test/opt/plenary.nvim
|
export VIMRUNTIME := $(shell nvim -u NORC --headless +'echo $$VIMRUNTIME' +'quitall' 2>&1)
|
||||||
|
|
||||||
all: lint test
|
all: lint fmt-check test
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
lua-language-server --check=lua/u/ --checklevel=Hint
|
@echo "## Typechecking"
|
||||||
lux check
|
@emmylua_check .
|
||||||
|
|
||||||
|
fmt-check:
|
||||||
|
@echo "## Checking code format"
|
||||||
|
@stylua --check .
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
stylua .
|
@echo "## Formatting code"
|
||||||
|
@stylua .
|
||||||
|
|
||||||
test: $(PLENARY_DIR)
|
test:
|
||||||
NVIM_APPNAME=noplugstest nvim -u NORC --headless -c 'set packpath+=~/.local/share/nvim/site' -c 'packadd plenary.nvim' -c "PlenaryBustedDirectory spec/"
|
@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
|
||||||
|
|
||||||
$(PLENARY_DIR):
|
watch:
|
||||||
git clone https://github.com/nvim-lua/plenary.nvim/ $(PLENARY_DIR)
|
@watchexec -c -e lua make
|
||||||
|
|||||||
135
README.md
135
README.md
@@ -1,26 +1,40 @@
|
|||||||
# u.nvim
|
# u.nvim
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
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
|
## 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)
|
- **Rendering System**: a utility that can declaratively render NeoVim-specific
|
||||||
- **Signals**: a simple dependency tracking system that pairs well with the rendering utilities for creating reactive/interactive UIs in NeoVim.
|
hyperscript into a buffer, supporting creating/managing extmarks, highlights,
|
||||||
- **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).
|
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.
|
- **Code Writer**: Write code with automatic indentation and formatting.
|
||||||
- **Operator Key Mapping**: Flexible key mapping that works with the selected text.
|
- **Operator Key Mapping**: Flexible key mapping that works with the selected
|
||||||
- **Text and Position Utilities**: Convenient functions to manage text objects and cursor positions.
|
text.
|
||||||
|
- **Text and Position Utilities**: Convenient functions to manage text objects
|
||||||
|
and cursor positions.
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
lazy.nvim:
|
This being a library, and not a proper plugin, it is recommended that you
|
||||||
```lua
|
vendor the specific version of this library that you need, including it in your
|
||||||
-- Setting `lazy = true` ensures that the library is only loaded
|
code. Package managers are a developing landscape for Lua in the context of
|
||||||
-- when `require 'u.<utility>' is called.
|
NeoVim. Perhaps in the future, `lux` will eliminate the need to vendor this
|
||||||
{ 'jrop/u.nvim', lazy = true }
|
library in your application code.
|
||||||
```
|
|
||||||
|
|
||||||
## Signal and Rendering Usage
|
## Signal and Rendering Usage
|
||||||
|
|
||||||
@@ -111,7 +125,9 @@ end)
|
|||||||
|
|
||||||
### `u.tracker`
|
### `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
|
```lua
|
||||||
local tracker = require('u.tracker')
|
local tracker = require('u.tracker')
|
||||||
@@ -134,7 +150,8 @@ 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
|
minimal set of changes in order to transform the current buffer text into the
|
||||||
desired state.
|
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
|
```lua
|
||||||
local h = require('u.renderer').h
|
local h = require('u.renderer').h
|
||||||
@@ -196,7 +213,8 @@ 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
|
```lua
|
||||||
local Renderer = require('u.renderer').Renderer
|
local Renderer = require('u.renderer').Renderer
|
||||||
@@ -219,22 +237,37 @@ buf:render {
|
|||||||
|
|
||||||
<blockquote>
|
<blockquote>
|
||||||
<del>
|
<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>
|
</del>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
<b>This has changed in v2</b>. After much thought, I realized that:
|
<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.
|
1. The 0-based indexing in NeoVim is prevelant in the `:api`, which is designed
|
||||||
2. This is a Lua library (surprise, surprise, duh) - the idioms of the language should take precedence over my preference
|
to be exposed to many languages. As such, it makes sense for this interface
|
||||||
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.
|
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
|
### 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
|
```lua
|
||||||
local Range = require 'u.range'
|
local Range = require 'u.range'
|
||||||
@@ -245,7 +278,9 @@ Range.new(start, stop, 'v') -- charwise selection
|
|||||||
Range.new(start, stop, 'V') -- linewise 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
|
```lua
|
||||||
-- get the first line in a buffer:
|
-- get the first line in a buffer:
|
||||||
@@ -260,7 +295,8 @@ Range.from_motion('iW')
|
|||||||
Range.from_motion('a"')
|
Range.from_motion('a"')
|
||||||
|
|
||||||
-- Get the currently visually selected text:
|
-- 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()
|
Range.from_vtext()
|
||||||
|
|
||||||
--
|
--
|
||||||
@@ -272,7 +308,8 @@ function MyOpFunc(ty)
|
|||||||
local range = Range.from_op_func(ty)
|
local range = Range.from_op_func(ty)
|
||||||
-- do something with the range
|
-- do something with the range
|
||||||
end
|
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.keymap.set('<Leader>to', function()
|
||||||
vim.g.operatorfunc = 'v:lua.MyOpFunc'
|
vim.g.operatorfunc = 'v:lua.MyOpFunc'
|
||||||
return 'g@'
|
return 'g@'
|
||||||
@@ -281,7 +318,8 @@ end, { expr = true })
|
|||||||
--
|
--
|
||||||
-- Commands:
|
-- 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)
|
vim.api.nvim_create_user_command('MyCmd', function(args)
|
||||||
local range = Range.from_cmd_args(args)
|
local range = Range.from_cmd_args(args)
|
||||||
if range == nil then
|
if range == nil then
|
||||||
@@ -292,7 +330,8 @@ vim.api.nvim_create_user_command('MyCmd', function(args)
|
|||||||
end, { range = true })
|
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
|
```lua
|
||||||
local range = ...
|
local range = ...
|
||||||
@@ -362,27 +401,39 @@ Access and manipulate buffers easily:
|
|||||||
```lua
|
```lua
|
||||||
local Buffer = require 'u.buffer'
|
local Buffer = require 'u.buffer'
|
||||||
local buf = Buffer.current()
|
local buf = Buffer.current()
|
||||||
buf:line_count() -- the number of lines in the current buffer
|
buf.b.<option> -- get buffer-local variables
|
||||||
buf:get_option '...'
|
buf.b.<option> = ... -- set buffer-local variables
|
||||||
buf:set_option('...', ...)
|
buf.bo.<option> -- get buffer options
|
||||||
buf:get_var '...'
|
buf.bo.<option> = ... -- set buffer options
|
||||||
buf:set_var('...', ...)
|
buf:line_count() -- the number of lines in the current buffer
|
||||||
buf:all() -- returns a Range representing the entire buffer
|
buf:all() -- returns a Range representing the entire buffer
|
||||||
buf:is_empty() -- returns true if the buffer has no text
|
buf:is_empty() -- returns true if the buffer has no text
|
||||||
buf:append_line '...'
|
buf:append_line '...'
|
||||||
buf:line(1) -- returns a Range representing the first line in the buffer
|
buf:line(1) -- returns a Range representing the first line in the buffer
|
||||||
buf:line(-1) -- returns a Range representing the last line in the buffer
|
buf:line(-1) -- returns a Range representing the last line in the buffer
|
||||||
buf:lines(1, 2) -- returns a Range representing the first two lines in the buffer
|
buf:lines(1, 2) -- returns a Range representing the first two lines in the buffer
|
||||||
buf:lines(2, -2) -- returns a Range representing all but the first and last lines of a buffer
|
buf:lines(2, -2) -- returns a Range representing all but the first and last lines of a buffer
|
||||||
buf:txtobj('iw') -- returns a Range representing the text object 'iw' in the give buffer
|
buf:txtobj('iw') -- returns a Range representing the text object 'iw' in the give buffer
|
||||||
```
|
```
|
||||||
|
|
||||||
## License (MIT)
|
## License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2024 jrapodaca@gmail.com
|
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.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
local tracker = require 'u.tracker'
|
|
||||||
local Buffer = require 'u.buffer'
|
local Buffer = require 'u.buffer'
|
||||||
|
local tracker = require 'u.tracker'
|
||||||
local h = require('u.renderer').h
|
local h = require('u.renderer').h
|
||||||
|
|
||||||
-- Create an buffer for the UI
|
-- Create an buffer for the UI
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
-- change on the underlying filesystem.
|
-- change on the underlying filesystem.
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
--- @alias FsDir { kind: 'dir'; path: string; expanded: boolean; children: FsNode[] }
|
--- @alias u.examples.FsDir { kind: 'dir', path: string, expanded: boolean, children: u.examples.FsNode[] }
|
||||||
--- @alias FsFile { kind: 'file'; path: string }
|
--- @alias u.examples.FsFile { kind: 'file', path: string }
|
||||||
--- @alias FsNode FsDir | FsFile
|
--- @alias u.examples.FsNode u.examples.FsDir | u.examples.FsFile
|
||||||
--- @alias ShowOpts { root_path?: string, width?: number, focus_path?: string }
|
--- @alias u.examples.ShowOpts { root_path?: string, width?: number, focus_path?: string }
|
||||||
|
|
||||||
local Buffer = require 'u.buffer'
|
local Buffer = require 'u.buffer'
|
||||||
local Renderer = require('u.renderer').Renderer
|
local Renderer = require('u.renderer').Renderer
|
||||||
@@ -44,11 +44,7 @@ end
|
|||||||
|
|
||||||
--- Normalizes the given path to an absolute path.
|
--- Normalizes the given path to an absolute path.
|
||||||
--- @param path string
|
--- @param path string
|
||||||
function H.normalize(path)
|
function H.normalize(path) return vim.fs.abspath(vim.fs.normalize(path)) end
|
||||||
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`.
|
--- Computes the relative path from `base` to `path`.
|
||||||
--- @param path string
|
--- @param path string
|
||||||
@@ -62,13 +58,13 @@ function H.relative(path, base)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- @param root_path string
|
--- @param root_path string
|
||||||
--- @return { tree: FsDir; path_to_node: table<string, FsNode> }
|
--- @return { tree: u.examples.FsDir, path_to_node: table<string, u.examples.FsNode> }
|
||||||
function H.get_tree_inf(root_path)
|
function H.get_tree_inf(root_path)
|
||||||
logger:info { 'get_tree_inf', root_path }
|
logger:info { 'get_tree_inf', root_path }
|
||||||
--- @type table<string, FsNode>
|
--- @type table<string, u.examples.FsNode>
|
||||||
local path_to_node = {}
|
local path_to_node = {}
|
||||||
|
|
||||||
--- @type FsDir
|
--- @type u.examples.FsDir
|
||||||
local tree = {
|
local tree = {
|
||||||
kind = 'dir',
|
kind = 'dir',
|
||||||
path = H.normalize(root_path or '.'),
|
path = H.normalize(root_path or '.'),
|
||||||
@@ -81,8 +77,8 @@ function H.get_tree_inf(root_path)
|
|||||||
return { tree = tree, path_to_node = path_to_node }
|
return { tree = tree, path_to_node = path_to_node }
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param tree FsDir
|
--- @param tree u.examples.FsDir
|
||||||
--- @param path_to_node table<string, FsNode>
|
--- @param path_to_node table<string, u.examples.FsNode>
|
||||||
function H.populate_dir_children(tree, path_to_node)
|
function H.populate_dir_children(tree, path_to_node)
|
||||||
tree.children = {}
|
tree.children = {}
|
||||||
|
|
||||||
@@ -117,15 +113,18 @@ function H.populate_dir_children(tree, path_to_node)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- @param opts {
|
--- @param opts {
|
||||||
--- bufnr: number;
|
--- bufnr: number,
|
||||||
--- prev_winnr: number;
|
--- prev_winnr: number,
|
||||||
--- root_path: string;
|
--- root_path: string,
|
||||||
--- focus_path?: string;
|
--- focus_path?: string
|
||||||
--- }
|
--- }
|
||||||
---
|
---
|
||||||
--- @return { expand: fun(path: string), collapse: fun(path: string) }
|
--- @return { expand: fun(path: string), collapse: fun(path: string) }
|
||||||
local function _render_in_buffer(opts)
|
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_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))
|
local s_focused_path = tracker.create_signal(H.normalize(opts.focus_path or opts.root_path))
|
||||||
|
|
||||||
@@ -136,7 +135,7 @@ local function _render_in_buffer(opts)
|
|||||||
local parts = H.split_path(H.relative(focused_path, tree_inf.tree.path))
|
local parts = H.split_path(H.relative(focused_path, tree_inf.tree.path))
|
||||||
local path_to_node = tree_inf.path_to_node
|
local path_to_node = tree_inf.path_to_node
|
||||||
|
|
||||||
--- @param node FsDir
|
--- @param node u.examples.FsDir
|
||||||
--- @param child_names string[]
|
--- @param child_names string[]
|
||||||
local function expand_to(node, child_names)
|
local function expand_to(node, child_names)
|
||||||
if #child_names == 0 then return end
|
if #child_names == 0 then return end
|
||||||
@@ -155,19 +154,35 @@ local function _render_in_buffer(opts)
|
|||||||
end)
|
end)
|
||||||
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))
|
|
||||||
--
|
--
|
||||||
-- -- TODO: proper disposal
|
-- :help watch-file
|
||||||
-- watcher:stop()
|
--
|
||||||
-- end)
|
local watcher = vim.uv.new_fs_event()
|
||||||
-- end
|
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,
|
||||||
|
})
|
||||||
|
|
||||||
local controller = {}
|
local controller = {}
|
||||||
|
|
||||||
@@ -295,7 +310,7 @@ local function _render_in_buffer(opts)
|
|||||||
--
|
--
|
||||||
local renderer = Renderer.new(opts.bufnr)
|
local renderer = Renderer.new(opts.bufnr)
|
||||||
tracker.create_effect(function()
|
tracker.create_effect(function()
|
||||||
--- @type { tree: FsDir; path_to_node: table<string, FsNode> }
|
--- @type { tree: u.examples.FsDir, path_to_node: table<string, u.examples.FsNode> }
|
||||||
local tree_inf = s_tree_inf:get()
|
local tree_inf = s_tree_inf:get()
|
||||||
local tree = tree_inf.tree
|
local tree = tree_inf.tree
|
||||||
|
|
||||||
@@ -314,7 +329,7 @@ local function _render_in_buffer(opts)
|
|||||||
|
|
||||||
--- Since the filesystem is a recursive tree of nodes, we need to
|
--- Since the filesystem is a recursive tree of nodes, we need to
|
||||||
--- recursively render each node. This function does just that:
|
--- recursively render each node. This function does just that:
|
||||||
--- @param node FsNode
|
--- @param node u.examples.FsNode
|
||||||
--- @param level number
|
--- @param level number
|
||||||
local function render_node(node, level)
|
local function render_node(node, level)
|
||||||
local name = vim.fs.basename(node.path)
|
local name = vim.fs.basename(node.path)
|
||||||
@@ -331,7 +346,11 @@ local function _render_in_buffer(opts)
|
|||||||
return ''
|
return ''
|
||||||
end,
|
end,
|
||||||
n = function()
|
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 ''
|
return ''
|
||||||
end,
|
end,
|
||||||
r = function()
|
r = function()
|
||||||
@@ -351,7 +370,11 @@ local function _render_in_buffer(opts)
|
|||||||
local icon = node.expanded and '' or ''
|
local icon = node.expanded and '' or ''
|
||||||
tb:put {
|
tb:put {
|
||||||
current_line > 1 and '\n',
|
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
|
if node.expanded then
|
||||||
for _, child in ipairs(node.children) do
|
for _, child in ipairs(node.children) do
|
||||||
@@ -384,14 +407,14 @@ end
|
|||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
--- @type {
|
--- @type {
|
||||||
--- bufnr: number;
|
--- bufnr: number,
|
||||||
--- winnr: number;
|
--- winnr: number,
|
||||||
--- controller: { expand: fun(path: string), collapse: fun(path: string) };
|
--- controller: { expand: fun(path: string), collapse: fun(path: string) }
|
||||||
--- } | nil
|
--- } | nil
|
||||||
local current_inf = nil
|
local current_inf = nil
|
||||||
|
|
||||||
--- Show the filetree:
|
--- Show the filetree:
|
||||||
--- @param opts? ShowOpts
|
--- @param opts? u.examples.ShowOpts
|
||||||
function M.show(opts)
|
function M.show(opts)
|
||||||
if current_inf ~= nil then return current_inf.controller end
|
if current_inf ~= nil then return current_inf.controller end
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
@@ -411,8 +434,8 @@ function M.show(opts)
|
|||||||
callback = M.hide,
|
callback = M.hide,
|
||||||
})
|
})
|
||||||
|
|
||||||
vim.wo.number = false
|
vim.wo[0][0].number = false
|
||||||
vim.wo.relativenumber = false
|
vim.wo[0][0].relativenumber = false
|
||||||
|
|
||||||
local bufnr = vim.api.nvim_get_current_buf()
|
local bufnr = vim.api.nvim_get_current_buf()
|
||||||
|
|
||||||
@@ -433,7 +456,7 @@ function M.hide()
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Toggle the filetree:
|
--- Toggle the filetree:
|
||||||
--- @param opts? ShowOpts
|
--- @param opts? u.examples.ShowOpts
|
||||||
function M.toggle(opts)
|
function M.toggle(opts)
|
||||||
if current_inf == nil then
|
if current_inf == nil then
|
||||||
M.show(opts)
|
M.show(opts)
|
||||||
|
|||||||
117
examples/form.lua
Normal file
117
examples/form.lua
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
-- form.lua:
|
||||||
|
--
|
||||||
|
-- This is a runnable example of a form. Open this file in Neovim, and execute
|
||||||
|
-- `:luafile %` to run it. It will create a new buffer to the side, and render
|
||||||
|
-- an interactive form. Edit the "inputs" between the `[...]` brackets, and
|
||||||
|
-- watch the buffer react immediately to your changes.
|
||||||
|
--
|
||||||
|
|
||||||
|
local Renderer = require('u.renderer').Renderer
|
||||||
|
local h = require('u.renderer').h
|
||||||
|
local tracker = require 'u.tracker'
|
||||||
|
|
||||||
|
-- Create a new, temporary, buffer to the side:
|
||||||
|
vim.cmd.vnew()
|
||||||
|
vim.bo.buftype = 'nofile'
|
||||||
|
vim.bo.bufhidden = 'wipe'
|
||||||
|
vim.bo.buflisted = false
|
||||||
|
local renderer = Renderer.new()
|
||||||
|
|
||||||
|
-- Create two signals:
|
||||||
|
local s_name = tracker.create_signal 'whoever-you-are'
|
||||||
|
local s_age = tracker.create_signal 'ideally-a-number'
|
||||||
|
|
||||||
|
-- We can create derived information from the signals above. Say we want to do
|
||||||
|
-- some validation on the input for `age`: we can do that with a memo:
|
||||||
|
local s_age_info = tracker.create_memo(function()
|
||||||
|
local age_raw = s_age:get()
|
||||||
|
local age_digits = age_raw:match '^%s*(%d+)%s*$'
|
||||||
|
local age_n = age_digits and tonumber(age_digits) or nil
|
||||||
|
return {
|
||||||
|
type = age_n and 'number' or 'string',
|
||||||
|
raw = age_raw,
|
||||||
|
n = age_n,
|
||||||
|
n1 = age_n and age_n + 1 or nil,
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- This is the render effect that depends on the signals created above. This
|
||||||
|
-- will re-run every time one of the signals changes.
|
||||||
|
tracker.create_effect(function()
|
||||||
|
local name = s_name:get()
|
||||||
|
local age = s_age:get()
|
||||||
|
local age_info = s_age_info:get()
|
||||||
|
|
||||||
|
-- Each time the signals change, we re-render the buffer:
|
||||||
|
renderer:render {
|
||||||
|
h.Type({}, '# Form Example'),
|
||||||
|
'\n\n',
|
||||||
|
|
||||||
|
-- We can also listen for when specific locations in the buffer change, on
|
||||||
|
-- a tag-by-tag basis. This gives us two-way data-binding between the
|
||||||
|
-- buffer and the signals.
|
||||||
|
{
|
||||||
|
'Name: ',
|
||||||
|
h.Structure({
|
||||||
|
on_change = function(text) s_name:set(text) end,
|
||||||
|
}, name),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'\nAge: ',
|
||||||
|
h.Structure({
|
||||||
|
on_change = function(text) s_age:set(text) end,
|
||||||
|
}, age),
|
||||||
|
},
|
||||||
|
|
||||||
|
'\n\n',
|
||||||
|
|
||||||
|
-- Show the values of the signals here, too, so that we can see the
|
||||||
|
-- reactivity in action. If you change the values in the tags above, you
|
||||||
|
-- can see the changes reflected here immediately.
|
||||||
|
{ 'Hello, "', name, '"!' },
|
||||||
|
|
||||||
|
--
|
||||||
|
-- A more complex example: we can do much more complex rendering, based on
|
||||||
|
-- the state. For example, if you type different values into the `age`
|
||||||
|
-- field, you can see not only the displayed information change, but also
|
||||||
|
-- the color of the highlights in this section will adapt to the type of
|
||||||
|
-- information that has been detected.
|
||||||
|
--
|
||||||
|
-- If string input is detected, values below are shown in the
|
||||||
|
-- `String`/`ErrorMsg` highlight groups.
|
||||||
|
--
|
||||||
|
-- If number input is detected, values below are shown in the `Number`
|
||||||
|
-- highlight group.
|
||||||
|
--
|
||||||
|
-- If a valid number is entered, then this section also displays how old
|
||||||
|
-- you willl be next year (`n + 1`).
|
||||||
|
--
|
||||||
|
|
||||||
|
'\n\n',
|
||||||
|
h.Type({}, '## Computed Information (derived from `age`)'),
|
||||||
|
'\n\n',
|
||||||
|
{
|
||||||
|
'Type: ',
|
||||||
|
h('text', {
|
||||||
|
hl = age_info.type == 'number' and 'Number' or 'String',
|
||||||
|
}, age_info.type),
|
||||||
|
},
|
||||||
|
{ '\nRaw input: ', h.String({}, '"' .. age_info.raw .. '"') },
|
||||||
|
{
|
||||||
|
'\nCurrent age: ',
|
||||||
|
age_info.n
|
||||||
|
-- Show the age:
|
||||||
|
and h.Number({}, tostring(age_info.n))
|
||||||
|
-- Show an error-placeholder if the age is invalid:
|
||||||
|
or h.ErrorMsg({}, '(?)'),
|
||||||
|
},
|
||||||
|
|
||||||
|
-- This part is shown conditionally, i.e., only if the age next year can be
|
||||||
|
-- computed:
|
||||||
|
age_info.n1
|
||||||
|
and {
|
||||||
|
'\nAge next year: ',
|
||||||
|
h.Number({}, tostring(age_info.n1)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end)
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
local Buffer = require 'u.buffer'
|
local Renderer = require('u.renderer').Renderer
|
||||||
local TreeBuilder = require('u.renderer').TreeBuilder
|
local TreeBuilder = require('u.renderer').TreeBuilder
|
||||||
local tracker = require 'u.tracker'
|
local tracker = require 'u.tracker'
|
||||||
local utils = require 'u.utils'
|
local utils = require 'u.utils'
|
||||||
local Window = require 'my.window'
|
|
||||||
|
|
||||||
local TIMEOUT = 4000
|
local TIMEOUT = 4000
|
||||||
local ICONS = {
|
local ICONS = {
|
||||||
@@ -14,15 +13,25 @@ local ICONS = {
|
|||||||
}
|
}
|
||||||
local DEFAULT_ICON = { text = '', group = 'DiagnosticSignOk' }
|
local DEFAULT_ICON = { text = '', group = 'DiagnosticSignOk' }
|
||||||
|
|
||||||
--- @alias Notification {
|
local S_EDITOR_DIMENSIONS =
|
||||||
--- kind: number;
|
tracker.create_signal(utils.get_editor_dimensions(), 's:editor_dimensions')
|
||||||
--- id: number;
|
vim.api.nvim_create_autocmd('VimResized', {
|
||||||
--- text: string;
|
callback = function()
|
||||||
|
local new_dim = utils.get_editor_dimensions()
|
||||||
|
S_EDITOR_DIMENSIONS:set(new_dim)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
--- @alias u.examples.Notification {
|
||||||
|
--- kind: number,
|
||||||
|
--- id: number,
|
||||||
|
--- text: string,
|
||||||
|
--- timer: uv.uv_timer_t
|
||||||
--- }
|
--- }
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
--- @type Window | nil
|
--- @type { win: integer, buf: integer, renderer: u.renderer.Renderer } | nil
|
||||||
local notifs_w
|
local notifs_w
|
||||||
|
|
||||||
local s_notifications_raw = tracker.create_signal {}
|
local s_notifications_raw = tracker.create_signal {}
|
||||||
@@ -30,44 +39,49 @@ local s_notifications = s_notifications_raw:debounce(50)
|
|||||||
|
|
||||||
-- Render effect:
|
-- Render effect:
|
||||||
tracker.create_effect(function()
|
tracker.create_effect(function()
|
||||||
--- @type Notification[]
|
--- @type u.examples.Notification[]
|
||||||
local notifs = s_notifications:get()
|
local notifs = s_notifications:get()
|
||||||
|
--- @type { width: integer, height: integer }
|
||||||
|
local editor_size = S_EDITOR_DIMENSIONS:get()
|
||||||
|
|
||||||
if #notifs == 0 then
|
if #notifs == 0 then
|
||||||
if notifs_w then
|
if notifs_w then
|
||||||
notifs_w:close(true)
|
if vim.api.nvim_win_is_valid(notifs_w.win) then vim.api.nvim_win_close(notifs_w.win, true) end
|
||||||
notifs_w = nil
|
notifs_w = nil
|
||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local avail_width = editor_size.width
|
||||||
|
local float_width = 40
|
||||||
|
local float_height = math.min(#notifs, editor_size.height - 3)
|
||||||
|
local win_config = {
|
||||||
|
relative = 'editor',
|
||||||
|
anchor = 'NE',
|
||||||
|
row = 0,
|
||||||
|
col = avail_width,
|
||||||
|
width = float_width,
|
||||||
|
height = float_height,
|
||||||
|
border = 'single',
|
||||||
|
focusable = false,
|
||||||
|
zindex = 900,
|
||||||
|
}
|
||||||
vim.schedule(function()
|
vim.schedule(function()
|
||||||
local editor_size = utils.get_editor_dimensions()
|
|
||||||
local avail_width = editor_size.width
|
|
||||||
local float_width = 40
|
|
||||||
local win_config = {
|
|
||||||
relative = 'editor',
|
|
||||||
anchor = 'NE',
|
|
||||||
row = 0,
|
|
||||||
col = avail_width,
|
|
||||||
width = float_width,
|
|
||||||
height = math.min(#notifs, editor_size.height - 3),
|
|
||||||
border = 'single',
|
|
||||||
focusable = false,
|
|
||||||
}
|
|
||||||
if not notifs_w or not vim.api.nvim_win_is_valid(notifs_w.win) then
|
if not notifs_w or not vim.api.nvim_win_is_valid(notifs_w.win) then
|
||||||
notifs_w = Window.new(Buffer.create(false, true), win_config)
|
local b = vim.api.nvim_create_buf(false, true)
|
||||||
vim.wo[notifs_w.win].cursorline = false
|
local w = vim.api.nvim_open_win(b, false, win_config)
|
||||||
vim.wo[notifs_w.win].list = false
|
vim.wo[w].cursorline = false
|
||||||
vim.wo[notifs_w.win].listchars = ''
|
vim.wo[w].list = false
|
||||||
vim.wo[notifs_w.win].number = false
|
vim.wo[w].listchars = ''
|
||||||
vim.wo[notifs_w.win].relativenumber = false
|
vim.wo[w].number = false
|
||||||
vim.wo[notifs_w.win].wrap = false
|
vim.wo[w].relativenumber = false
|
||||||
|
vim.wo[w].wrap = false
|
||||||
|
notifs_w = { win = w, buf = b, renderer = Renderer.new(b) }
|
||||||
else
|
else
|
||||||
notifs_w:set_config(win_config)
|
vim.api.nvim_win_set_config(notifs_w.win, win_config)
|
||||||
end
|
end
|
||||||
|
|
||||||
notifs_w:render(TreeBuilder.new()
|
notifs_w.renderer:render(TreeBuilder.new()
|
||||||
:nest(function(tb)
|
:nest(function(tb)
|
||||||
for idx, notif in ipairs(notifs) do
|
for idx, notif in ipairs(notifs) do
|
||||||
if idx > 1 then tb:put '\n' end
|
if idx > 1 then tb:put '\n' end
|
||||||
@@ -79,48 +93,81 @@ tracker.create_effect(function()
|
|||||||
end)
|
end)
|
||||||
:tree())
|
:tree())
|
||||||
vim.api.nvim_win_call(notifs_w.win, function()
|
vim.api.nvim_win_call(notifs_w.win, function()
|
||||||
-- scroll to bottom:
|
vim.fn.winrestview {
|
||||||
vim.cmd.normal 'G'
|
-- scroll all the way left:
|
||||||
-- scroll all the way to the left:
|
leftcol = 0,
|
||||||
vim.cmd.normal '9999zh'
|
-- set the bottom line to be at the bottom of the window:
|
||||||
|
topline = vim.api.nvim_buf_line_count(notifs_w.buf) - win_config.height + 1,
|
||||||
|
}
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
--- @param id number
|
||||||
|
local function _delete_notif(id)
|
||||||
|
--- @param notifs u.examples.Notification[]
|
||||||
|
s_notifications_raw:schedule_update(function(notifs)
|
||||||
|
for i, notif in ipairs(notifs) do
|
||||||
|
if notif.id == id then
|
||||||
|
notif.timer:stop()
|
||||||
|
notif.timer:close()
|
||||||
|
table.remove(notifs, i)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return notifs
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
local _orig_notify
|
local _orig_notify
|
||||||
|
|
||||||
--- @param msg string
|
--- @param msg string
|
||||||
--- @param level integer|nil
|
--- @param level integer|nil
|
||||||
--- @param opts table|nil
|
--- @param opts? { id: number }
|
||||||
local function my_notify(msg, level, opts)
|
function M.notify(msg, level, opts)
|
||||||
vim.schedule(function() _orig_notify(msg, level, opts) end)
|
|
||||||
if level == nil then level = vim.log.levels.INFO end
|
if level == nil then level = vim.log.levels.INFO end
|
||||||
if level < vim.log.levels.INFO then return end
|
|
||||||
|
|
||||||
local id = math.random(math.huge)
|
opts = opts or {}
|
||||||
|
local id = opts.id or math.random(999999999)
|
||||||
|
|
||||||
--- @param notifs Notification[]
|
--- @type u.examples.Notification?
|
||||||
s_notifications_raw:schedule_update(function(notifs)
|
local notif = vim.iter(s_notifications_raw:get()):find(function(n) return n.id == id end)
|
||||||
table.insert(notifs, { kind = level, id = id, text = msg })
|
if not notif then
|
||||||
return notifs
|
-- Create a new notification (maybe):
|
||||||
end)
|
if vim.trim(msg) == '' then return id end
|
||||||
|
if level < vim.log.levels.INFO then return id end
|
||||||
|
|
||||||
vim.defer_fn(function()
|
local timer = assert((vim.uv or vim.loop).new_timer(), 'could not create timer')
|
||||||
--- @param notifs Notification[]
|
timer:start(TIMEOUT, 0, function() _delete_notif(id) end)
|
||||||
|
notif = {
|
||||||
|
id = id,
|
||||||
|
kind = level,
|
||||||
|
text = msg,
|
||||||
|
timer = timer,
|
||||||
|
}
|
||||||
|
--- @param notifs u.examples.Notification[]
|
||||||
s_notifications_raw:schedule_update(function(notifs)
|
s_notifications_raw:schedule_update(function(notifs)
|
||||||
for i, notif in ipairs(notifs) do
|
table.insert(notifs, notif)
|
||||||
if notif.id == id then
|
|
||||||
table.remove(notifs, i)
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return notifs
|
return notifs
|
||||||
end)
|
end)
|
||||||
end, TIMEOUT)
|
else
|
||||||
|
-- Update an existing notification:
|
||||||
|
s_notifications_raw:schedule_update(function(notifs)
|
||||||
|
-- We already have a copy-by-reference of the notif we want to modify:
|
||||||
|
notif.timer:stop()
|
||||||
|
notif.text = msg
|
||||||
|
notif.kind = level
|
||||||
|
notif.timer:start(TIMEOUT, 0, function() _delete_notif(id) end)
|
||||||
|
|
||||||
|
return notifs
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return id
|
||||||
end
|
end
|
||||||
|
|
||||||
local _once_msgs = {}
|
local _once_msgs = {}
|
||||||
local function my_notify_once(msg, level, opts)
|
function M.notify_once(msg, level, opts)
|
||||||
if vim.tbl_contains(_once_msgs, msg) then return false end
|
if vim.tbl_contains(_once_msgs, msg) then return false end
|
||||||
table.insert(_once_msgs, msg)
|
table.insert(_once_msgs, msg)
|
||||||
vim.notify(msg, level, opts)
|
vim.notify(msg, level, opts)
|
||||||
@@ -130,8 +177,8 @@ end
|
|||||||
function M.setup()
|
function M.setup()
|
||||||
if _orig_notify == nil then _orig_notify = vim.notify end
|
if _orig_notify == nil then _orig_notify = vim.notify end
|
||||||
|
|
||||||
vim.notify = my_notify
|
vim.notify = M.notify
|
||||||
vim.notify_once = my_notify_once
|
vim.notify_once = M.notify_once
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
local utils = require 'u.utils'
|
|
||||||
local Buffer = require 'u.buffer'
|
local Buffer = require 'u.buffer'
|
||||||
|
local utils = require 'u.utils'
|
||||||
local Renderer = require('u.renderer').Renderer
|
local Renderer = require('u.renderer').Renderer
|
||||||
local h = require('u.renderer').h
|
local h = require('u.renderer').h
|
||||||
local TreeBuilder = require('u.renderer').TreeBuilder
|
local TreeBuilder = require('u.renderer').TreeBuilder
|
||||||
@@ -7,7 +7,8 @@ local tracker = require 'u.tracker'
|
|||||||
|
|
||||||
local M = {}
|
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', {
|
vim.api.nvim_create_autocmd('VimResized', {
|
||||||
callback = function()
|
callback = function()
|
||||||
local new_dim = utils.get_editor_dimensions()
|
local new_dim = utils.get_editor_dimensions()
|
||||||
@@ -43,26 +44,26 @@ local function shallow_copy_arr(arr) return vim.iter(arr):totable() end
|
|||||||
-- shortest portion of this function.
|
-- shortest portion of this function.
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
--- @alias SelectController {
|
--- @alias u.examples.SelectController<T> {
|
||||||
--- get_items: fun(): T[];
|
--- get_items: fun(): T[],
|
||||||
--- set_items: fun(items: T[]);
|
--- set_items: fun(items: T[]),
|
||||||
--- set_filter_text: fun(filter_text: string);
|
--- set_filter_text: fun(filter_text: string),
|
||||||
--- get_selected_indices: fun(): number[];
|
--- get_selected_indices: fun(): number[],
|
||||||
--- get_selected_items: fun(): T[];
|
--- get_selected_items: fun(): T[],
|
||||||
--- set_selected_indices: fun(indicies: number[], ephemeral?: boolean);
|
--- set_selected_indices: fun(indicies: number[], ephemeral?: boolean),
|
||||||
--- close: fun();
|
--- close: fun()
|
||||||
--- }
|
--- }
|
||||||
--- @alias SelectOpts<T> {
|
--- @alias u.examples.SelectOpts<T> {
|
||||||
--- items: `T`[];
|
--- items: `T`[],
|
||||||
--- multi?: boolean;
|
--- multi?: boolean,
|
||||||
--- format_item?: fun(item: T): Tree;
|
--- format_item?: (fun(item: T): u.renderer.Tree),
|
||||||
--- on_finish?: fun(items: T[], indicies: number[]);
|
--- on_finish?: (fun(items: T[], indicies: number[])),
|
||||||
--- on_selection_changed?: fun(items: T[], indicies: number[]);
|
--- on_selection_changed?: fun(items: T[], indicies: number[]),
|
||||||
--- mappings?: table<string, fun(select: SelectController)>;
|
--- mappings?: table<string, fun(select: u.examples.SelectController)>
|
||||||
--- }
|
--- }
|
||||||
|
|
||||||
--- @generic T
|
--- @generic T
|
||||||
--- @param opts SelectOpts<T>
|
--- @param opts u.examples.SelectOpts<T>
|
||||||
function M.create_picker(opts) -- {{{
|
function M.create_picker(opts) -- {{{
|
||||||
local is_in_insert_mode = vim.api.nvim_get_mode().mode:sub(1, 1) == 'i'
|
local is_in_insert_mode = vim.api.nvim_get_mode().mode:sub(1, 1) == 'i'
|
||||||
local stopinsert = not is_in_insert_mode
|
local stopinsert = not is_in_insert_mode
|
||||||
@@ -202,7 +203,9 @@ function M.create_picker(opts) -- {{{
|
|||||||
|
|
||||||
local s_filter_text_undebounced = tracker.create_signal('', 's:filter_text')
|
local s_filter_text_undebounced = tracker.create_signal('', 's:filter_text')
|
||||||
w_input_buf:autocmd('TextChangedI', {
|
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)
|
local s_filter_text = s_filter_text_undebounced:debounce(50)
|
||||||
|
|
||||||
@@ -211,10 +214,15 @@ function M.create_picker(opts) -- {{{
|
|||||||
--
|
--
|
||||||
|
|
||||||
local s_formatted_items = tracker.create_memo(function()
|
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()
|
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)
|
end)
|
||||||
|
|
||||||
-- When the filter text changes, update the filtered items:
|
-- When the filter text changes, update the filtered items:
|
||||||
@@ -223,8 +231,10 @@ function M.create_picker(opts) -- {{{
|
|||||||
local formatted_items = s_formatted_items:get()
|
local formatted_items = s_formatted_items:get()
|
||||||
local filter_text = vim.trim(s_filter_text:get()):lower()
|
local filter_text = vim.trim(s_filter_text:get()):lower()
|
||||||
|
|
||||||
local filter_pattern = ''
|
--- @type string
|
||||||
local use_plain_pattern = false
|
local filter_pattern
|
||||||
|
--- @type boolean
|
||||||
|
local use_plain_pattern
|
||||||
if #formatted_items > 250 and #filter_text <= 3 then
|
if #formatted_items > 250 and #filter_text <= 3 then
|
||||||
filter_pattern = filter_text
|
filter_pattern = filter_text
|
||||||
use_plain_pattern = true
|
use_plain_pattern = true
|
||||||
@@ -247,7 +257,9 @@ function M.create_picker(opts) -- {{{
|
|||||||
local new_filtered_items = vim
|
local new_filtered_items = vim
|
||||||
.iter(formatted_items)
|
.iter(formatted_items)
|
||||||
:enumerate()
|
: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)
|
:filter(function(inf)
|
||||||
if filter_text == '' then return true end
|
if filter_text == '' then return true end
|
||||||
local formatted_as_string = Renderer.markup_to_string({ tree = inf.formatted }):lower()
|
local formatted_as_string = Renderer.markup_to_string({ tree = inf.formatted }):lower()
|
||||||
@@ -255,9 +267,7 @@ function M.create_picker(opts) -- {{{
|
|||||||
formatted_strings[inf.orig_idx] = formatted_as_string
|
formatted_strings[inf.orig_idx] = formatted_as_string
|
||||||
if use_plain_pattern then
|
if use_plain_pattern then
|
||||||
local x, y = formatted_as_string:find(filter_pattern, 1, true)
|
local x, y = formatted_as_string:find(filter_pattern, 1, true)
|
||||||
if x ~= nil and y ~= nil then
|
if x ~= nil and y ~= nil then matches[inf.orig_idx] = formatted_as_string:sub(x, y) end
|
||||||
matches[inf.orig_idx] = formatted_as_string:sub(x, y)
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
matches[inf.orig_idx] = string.match(formatted_as_string, filter_pattern)
|
matches[inf.orig_idx] = string.match(formatted_as_string, filter_pattern)
|
||||||
end
|
end
|
||||||
@@ -316,11 +326,13 @@ function M.create_picker(opts) -- {{{
|
|||||||
safe_wrap(function()
|
safe_wrap(function()
|
||||||
local items = s_items:get()
|
local items = s_items:get()
|
||||||
local selected_indices = s_selected_indices:get()
|
local selected_indices = s_selected_indices:get()
|
||||||
--- @type { orig_idx: number; item: T }[]
|
--- @type { orig_idx: number, item: T }[]
|
||||||
local filtered_items = s_filtered_items:get()
|
local filtered_items = s_filtered_items:get()
|
||||||
local cursor_index = s_cursor_index:get()
|
local cursor_index = s_cursor_index:get()
|
||||||
local indices = shallow_copy_arr(selected_indices)
|
local indices = shallow_copy_arr(selected_indices)
|
||||||
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 {
|
return {
|
||||||
items = vim.iter(indices):map(function(i) return items[i] end):totable(),
|
items = vim.iter(indices):map(function(i) return items[i] end):totable(),
|
||||||
indices = indices,
|
indices = indices,
|
||||||
@@ -397,8 +409,18 @@ function M.create_picker(opts) -- {{{
|
|||||||
end
|
end
|
||||||
s_cursor_index:set(next_cursor_index)
|
s_cursor_index:set(next_cursor_index)
|
||||||
end
|
end
|
||||||
vim.keymap.set('i', '<C-n>', safe_wrap(action_next_line), { buffer = w_input_buf.bufnr, desc = 'Picker: next' })
|
vim.keymap.set(
|
||||||
vim.keymap.set('i', '<Down>', safe_wrap(action_next_line), { buffer = w_input_buf.bufnr, desc = 'Picker: next' })
|
'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 function action_prev_line()
|
||||||
local max_line = #s_filtered_items:get()
|
local max_line = #s_filtered_items:get()
|
||||||
@@ -406,8 +428,18 @@ 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
|
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)
|
s_cursor_index:set(next_cursor_index)
|
||||||
end
|
end
|
||||||
vim.keymap.set('i', '<C-p>', safe_wrap(action_prev_line), { buffer = w_input_buf.bufnr, desc = 'Picker: previous' })
|
vim.keymap.set(
|
||||||
vim.keymap.set('i', '<Up>', safe_wrap(action_prev_line), { buffer = w_input_buf.bufnr, desc = 'Picker: previous' })
|
'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(
|
vim.keymap.set(
|
||||||
'i',
|
'i',
|
||||||
@@ -417,7 +449,9 @@ function M.create_picker(opts) -- {{{
|
|||||||
|
|
||||||
local index = s_filtered_items:get()[s_cursor_index:get()].orig_idx
|
local index = s_filtered_items:get()[s_cursor_index:get()].orig_idx
|
||||||
if vim.tbl_contains(s_selected_indices:get(), index) then
|
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
|
else
|
||||||
local new_selected_indices = shallow_copy_arr(s_selected_indices:get())
|
local new_selected_indices = shallow_copy_arr(s_selected_indices:get())
|
||||||
table.insert(new_selected_indices, index)
|
table.insert(new_selected_indices, index)
|
||||||
@@ -429,7 +463,12 @@ function M.create_picker(opts) -- {{{
|
|||||||
)
|
)
|
||||||
|
|
||||||
for key, fn in pairs(opts.mappings or {}) do
|
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
|
end
|
||||||
|
|
||||||
-- Render:
|
-- Render:
|
||||||
@@ -438,7 +477,7 @@ function M.create_picker(opts) -- {{{
|
|||||||
local selected_indices = s_selected_indices:get()
|
local selected_indices = s_selected_indices:get()
|
||||||
local top_offset = s_top_offset:get()
|
local top_offset = s_top_offset:get()
|
||||||
local cursor_index = s_cursor_index:get()
|
local cursor_index = s_cursor_index:get()
|
||||||
--- @type { filtered_idx: number; orig_idx: number; item: T; formatted: string }[]
|
--- @type { filtered_idx: number, orig_idx: number, item: T, formatted: string }[]
|
||||||
local visible_items = s_visible_items:get()
|
local visible_items = s_visible_items:get()
|
||||||
|
|
||||||
-- The above has to run in the execution context for the signaling to work, but
|
-- The above has to run in the execution context for the signaling to work, but
|
||||||
@@ -499,10 +538,9 @@ function M.create_picker(opts) -- {{{
|
|||||||
if ephemeral == nil then ephemeral = false end
|
if ephemeral == nil then ephemeral = false end
|
||||||
|
|
||||||
if ephemeral and #indicies == 1 then
|
if ephemeral and #indicies == 1 then
|
||||||
local matching_filtered_item_idx, _ = vim
|
local matching_filtered_item_idx, _ = vim.iter(s_filtered_items:get()):enumerate():find(
|
||||||
.iter(s_filtered_items:get())
|
function(_idx, inf) return inf.orig_idx == indicies[1] end
|
||||||
: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
|
if matching_filtered_item_idx ~= nil then s_cursor_index:set(indicies[1]) end
|
||||||
else
|
else
|
||||||
if not opts.multi then
|
if not opts.multi then
|
||||||
@@ -519,7 +557,7 @@ function M.create_picker(opts) -- {{{
|
|||||||
return safe_run(function() H.finish(true) end)
|
return safe_run(function() H.finish(true) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return controller --[[@as SelectController]]
|
return controller --[[@as u.examples.SelectController]]
|
||||||
end -- }}}
|
end -- }}}
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
@@ -721,7 +759,10 @@ function M.files(opts) -- {{{
|
|||||||
-- fast laptop. Show a warning and truncate the list in this case.
|
-- fast laptop. Show a warning and truncate the list in this case.
|
||||||
if #lines >= opts.limit then
|
if #lines >= opts.limit then
|
||||||
if not job_inf.notified_over_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)
|
pcall(vim.fn.jobstop, job_inf.id)
|
||||||
job_inf.notified_over_limit = true
|
job_inf.notified_over_limit = true
|
||||||
end
|
end
|
||||||
@@ -750,24 +791,21 @@ function M.buffers() -- {{{
|
|||||||
-- ensure that `cwd` ends with a trailing slash:
|
-- ensure that `cwd` ends with a trailing slash:
|
||||||
if cwd[#cwd] ~= '/' then cwd = cwd .. '/' end
|
if cwd[#cwd] ~= '/' then cwd = cwd .. '/' end
|
||||||
|
|
||||||
--- @type { name: string; changed: number; bufnr: number }[]
|
--- @type { name: string, changed: number, bufnr: number }[]
|
||||||
local bufs = vim.fn.getbufinfo { buflisted = 1 }
|
local bufs = vim.fn.getbufinfo { buflisted = 1 }
|
||||||
|
|
||||||
M.create_picker {
|
M.create_picker {
|
||||||
multi = true,
|
multi = true,
|
||||||
items = bufs,
|
items = bufs,
|
||||||
|
|
||||||
--- @param item { name: string; changed: number; bufnr: number }
|
--- @param item { name: string, changed: number, bufnr: number }
|
||||||
format_item = function(item)
|
format_item = function(item)
|
||||||
local item_name = item.name
|
local item_name = item.name
|
||||||
if item_name == '' then item_name = '[No Name]' end
|
if item_name == '' then item_name = '[No Name]' end
|
||||||
-- trim leading `cwd` from the buffer name:
|
-- trim leading `cwd` from the buffer name:
|
||||||
if item_name:sub(1, #cwd) == cwd then item_name = item_name:sub(#cwd + 1) end
|
if item_name:sub(1, #cwd) == cwd then item_name = item_name:sub(#cwd + 1) end
|
||||||
|
|
||||||
return TreeBuilder.new()
|
return TreeBuilder.new():put(item.changed == 1 and '[+] ' or ' '):put(item_name):tree()
|
||||||
:put(item.changed == 1 and '[+] ' or ' ')
|
|
||||||
:put(item_name)
|
|
||||||
:tree()
|
|
||||||
end,
|
end,
|
||||||
|
|
||||||
--- @params items { bufnr: number }[]
|
--- @params items { bufnr: number }[]
|
||||||
@@ -880,7 +918,9 @@ function M.lsp_code_symbols() -- {{{
|
|||||||
local item = items[1]
|
local item = items[1]
|
||||||
|
|
||||||
-- Jump to the file/buffer:
|
-- 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
|
if buf ~= nil then
|
||||||
vim.api.nvim_win_set_buf(0, buf.bufnr)
|
vim.api.nvim_win_set_buf(0, buf.bufnr)
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
local vim_repeat = require 'u.repeat'
|
|
||||||
local CodeWriter = require 'u.codewriter'
|
local CodeWriter = require 'u.codewriter'
|
||||||
local Range = require 'u.range'
|
local Range = require 'u.range'
|
||||||
|
local vim_repeat = require 'u.repeat'
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
local vim_repeat = require 'u.repeat'
|
|
||||||
local Pos = require 'u.pos'
|
|
||||||
local Range = require 'u.range'
|
|
||||||
local Buffer = require 'u.buffer'
|
local Buffer = require 'u.buffer'
|
||||||
local CodeWriter = require 'u.codewriter'
|
local CodeWriter = require 'u.codewriter'
|
||||||
|
local Range = require 'u.range'
|
||||||
|
local vim_repeat = require 'u.repeat'
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
local ESC = vim.api.nvim_replace_termcodes('<Esc>', true, false, true)
|
||||||
|
|
||||||
local surrounds = {
|
local surrounds = {
|
||||||
[')'] = { left = '(', right = ')' },
|
[')'] = { left = '(', right = ')' },
|
||||||
['('] = { left = '( ', right = ' )' },
|
['('] = { left = '( ', right = ' )' },
|
||||||
@@ -20,10 +21,10 @@ local surrounds = {
|
|||||||
['`'] = { left = '`', right = '`' },
|
['`'] = { left = '`', right = '`' },
|
||||||
}
|
}
|
||||||
|
|
||||||
--- @type { left: string; right: string } | nil
|
--- @type { left: string, right: string } | nil
|
||||||
local CACHED_BOUNDS = nil
|
local CACHED_BOUNDS = nil
|
||||||
|
|
||||||
--- @return { left: string; right: string }|nil
|
--- @return { left: string, right: string }|nil
|
||||||
local function prompt_for_bounds()
|
local function prompt_for_bounds()
|
||||||
if vim_repeat.is_repeating() then
|
if vim_repeat.is_repeating() then
|
||||||
-- If we are repeating, we don't want to prompt for bounds, because
|
-- If we are repeating, we don't want to prompt for bounds, because
|
||||||
@@ -54,7 +55,7 @@ local function prompt_for_bounds()
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- @param range u.Range
|
--- @param range u.Range
|
||||||
--- @param bounds { left: string; right: string }
|
--- @param bounds { left: string, right: string }
|
||||||
local function do_surround(range, bounds)
|
local function do_surround(range, bounds)
|
||||||
local left = bounds.left
|
local left = bounds.left
|
||||||
local right = bounds.right
|
local right = bounds.right
|
||||||
@@ -126,7 +127,7 @@ function M.setup()
|
|||||||
|
|
||||||
do_surround(range, bounds)
|
do_surround(range, bounds)
|
||||||
-- this is a visual mapping: end in normal mode:
|
-- this is a visual mapping: end in normal mode:
|
||||||
vim.cmd { cmd = 'normal', args = { '' }, bang = true }
|
vim.cmd.normal(ESC)
|
||||||
end, { noremap = true, silent = true })
|
end, { noremap = true, silent = true })
|
||||||
|
|
||||||
-- Change
|
-- Change
|
||||||
@@ -169,29 +170,19 @@ function M.setup()
|
|||||||
local irange = Range.from_motion('i' .. from_c, { user_defined = true })
|
local irange = Range.from_motion('i' .. from_c, { user_defined = true })
|
||||||
if arange == nil or irange == nil then return end
|
if arange == nil or irange == nil then return end
|
||||||
|
|
||||||
local lrange = Range.new(arange.start, irange.start:must_next(-1))
|
local lrange, rrange = arange:difference(irange)
|
||||||
local rrange = Range.new(irange.stop:must_next(1), arange.stop)
|
if not lrange or not rrange then return end
|
||||||
|
|
||||||
rrange:replace(to.right)
|
rrange:replace(to.right)
|
||||||
lrange:replace(to.left)
|
lrange:replace(to.left)
|
||||||
else
|
else
|
||||||
-- replace `from.right` with `to.right`:
|
-- replace `from.right` with `to.right`:
|
||||||
local last_line = arange:line(-1):text()
|
local right_text = arange:sub(-1, -#from.right)
|
||||||
local from_right_match = last_line:match(vim.pesc(from.right) .. '$')
|
right_text:replace(to.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`:
|
-- replace `from.left` with `to.left`:
|
||||||
local first_line = arange:line(1):text()
|
local left_text = arange:sub(1, #from.left)
|
||||||
local from_left_match = first_line:match('^' .. vim.pesc(from.left))
|
left_text:replace(to.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)
|
end)
|
||||||
end, { noremap = true, silent = true })
|
end, { noremap = true, silent = true })
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
local txtobj = require 'u.txtobj'
|
local Buffer = require 'u.buffer'
|
||||||
local Pos = require 'u.pos'
|
local Pos = require 'u.pos'
|
||||||
local Range = require 'u.range'
|
local Range = require 'u.range'
|
||||||
local Buffer = require 'u.buffer'
|
local txtobj = require 'u.txtobj'
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
|||||||
1
library/busted
Submodule
1
library/busted
Submodule
Submodule library/busted added at 5ed85d0e01
@@ -2,20 +2,24 @@ local Range = require 'u.range'
|
|||||||
local Renderer = require('u.renderer').Renderer
|
local Renderer = require('u.renderer').Renderer
|
||||||
|
|
||||||
--- @class u.Buffer
|
--- @class u.Buffer
|
||||||
--- @field bufnr number
|
--- @field bufnr integer
|
||||||
--- @field private renderer u.Renderer
|
--- @field b vim.var_accessor
|
||||||
|
--- @field bo vim.bo
|
||||||
|
--- @field renderer u.renderer.Renderer
|
||||||
local Buffer = {}
|
local Buffer = {}
|
||||||
|
Buffer.__index = Buffer
|
||||||
|
|
||||||
--- @param bufnr? number
|
--- @param bufnr? number
|
||||||
--- @return u.Buffer
|
--- @return u.Buffer
|
||||||
function Buffer.from_nr(bufnr)
|
function Buffer.from_nr(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
|
||||||
|
|
||||||
local renderer = Renderer.new(bufnr)
|
local renderer = Renderer.new(bufnr)
|
||||||
return setmetatable({
|
return setmetatable({
|
||||||
bufnr = bufnr,
|
bufnr = bufnr,
|
||||||
|
b = vim.b[bufnr],
|
||||||
|
bo = vim.bo[bufnr],
|
||||||
renderer = renderer,
|
renderer = renderer,
|
||||||
}, { __index = Buffer })
|
}, Buffer)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @return u.Buffer
|
--- @return u.Buffer
|
||||||
@@ -24,25 +28,15 @@ function Buffer.current() return Buffer.from_nr(0) end
|
|||||||
--- @param listed boolean
|
--- @param listed boolean
|
||||||
--- @param scratch boolean
|
--- @param scratch boolean
|
||||||
--- @return u.Buffer
|
--- @return u.Buffer
|
||||||
function Buffer.create(listed, scratch) return Buffer.from_nr(vim.api.nvim_create_buf(listed, scratch)) end
|
function Buffer.create(listed, scratch)
|
||||||
|
return Buffer.from_nr(vim.api.nvim_create_buf(listed, scratch))
|
||||||
function Buffer:set_tmp_options()
|
|
||||||
self:set_option('bufhidden', 'delete')
|
|
||||||
self:set_option('buflisted', false)
|
|
||||||
self:set_option('buftype', 'nowrite')
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param nm string
|
function Buffer:set_tmp_options()
|
||||||
function Buffer:get_option(nm) return vim.api.nvim_get_option_value(nm, { buf = self.bufnr }) end
|
self.bo.bufhidden = 'delete'
|
||||||
|
self.bo.buflisted = false
|
||||||
--- @param nm string
|
self.bo.buftype = 'nowrite'
|
||||||
function Buffer:set_option(nm, val) return vim.api.nvim_set_option_value(nm, val, { buf = self.bufnr }) end
|
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:line_count() return vim.api.nvim_buf_line_count(self.bufnr) end
|
||||||
|
|
||||||
@@ -67,21 +61,71 @@ end
|
|||||||
--- @param stop number 1-based line index
|
--- @param stop number 1-based line index
|
||||||
function Buffer:lines(start, stop) return Range.from_lines(self.bufnr, start, stop) end
|
function Buffer:lines(start, stop) return Range.from_lines(self.bufnr, start, stop) end
|
||||||
|
|
||||||
--- @param txt_obj string
|
--- @param motion string
|
||||||
--- @param opts? { contains_cursor?: boolean; pos?: u.Pos }
|
--- @param opts? { contains_cursor?: boolean, pos?: u.Pos }
|
||||||
function Buffer:txtobj(txt_obj, opts)
|
function Buffer:motion(motion, opts)
|
||||||
opts = vim.tbl_extend('force', opts or {}, { bufnr = self.bufnr })
|
opts = vim.tbl_extend('force', opts or {}, { bufnr = self.bufnr })
|
||||||
return Range.from_motion(txt_obj, opts)
|
return Range.from_motion(motion, opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param event string|string[]
|
--- @param event vim.api.keyset.events|vim.api.keyset.events[]
|
||||||
--- @diagnostic disable-next-line: undefined-doc-name
|
--- @diagnostic disable-next-line: undefined-doc-name
|
||||||
--- @param opts vim.api.keyset.create_autocmd
|
--- @param opts vim.api.keyset.create_autocmd
|
||||||
function Buffer:autocmd(event, opts)
|
function Buffer:autocmd(event, opts)
|
||||||
vim.api.nvim_create_autocmd(event, vim.tbl_extend('force', opts, { buffer = self.bufnr }))
|
vim.api.nvim_create_autocmd(event, vim.tbl_extend('force', opts, { buffer = self.bufnr }))
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param tree Tree
|
--- @param fn function
|
||||||
|
function Buffer:call(fn) return vim.api.nvim_buf_call(self.bufnr, fn) end
|
||||||
|
|
||||||
|
--- @param tree u.renderer.Tree
|
||||||
function Buffer:render(tree) return self.renderer:render(tree) end
|
function Buffer:render(tree) return self.renderer:render(tree) end
|
||||||
|
|
||||||
|
--- 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
|
return Buffer
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ local Buffer = require 'u.buffer'
|
|||||||
--- @field indent_level number
|
--- @field indent_level number
|
||||||
--- @field indent_str string
|
--- @field indent_str string
|
||||||
local CodeWriter = {}
|
local CodeWriter = {}
|
||||||
|
CodeWriter.__index = CodeWriter
|
||||||
|
|
||||||
--- @param indent_level? number
|
--- @param indent_level? number
|
||||||
--- @param indent_str? string
|
--- @param indent_str? string
|
||||||
@@ -18,7 +19,7 @@ function CodeWriter.new(indent_level, indent_str)
|
|||||||
indent_level = indent_level,
|
indent_level = indent_level,
|
||||||
indent_str = indent_str,
|
indent_str = indent_str,
|
||||||
}
|
}
|
||||||
setmetatable(cw, { __index = CodeWriter })
|
setmetatable(cw, CodeWriter)
|
||||||
return cw
|
return cw
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -31,13 +32,14 @@ end
|
|||||||
--- @param line string
|
--- @param line string
|
||||||
--- @param bufnr? number
|
--- @param bufnr? number
|
||||||
function CodeWriter.from_line(line, bufnr)
|
function CodeWriter.from_line(line, bufnr)
|
||||||
if bufnr == nil then bufnr = vim.api.nvim_get_current_buf() end
|
if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
|
||||||
|
|
||||||
local ws = line:match '^%s*'
|
local ws = line:match '^%s*'
|
||||||
local expandtab = vim.api.nvim_get_option_value('expandtab', { buf = bufnr })
|
local expandtab = vim.api.nvim_get_option_value('expandtab', { buf = bufnr })
|
||||||
local shiftwidth = vim.api.nvim_get_option_value('shiftwidth', { buf = bufnr })
|
local shiftwidth = vim.api.nvim_get_option_value('shiftwidth', { buf = bufnr })
|
||||||
|
|
||||||
local indent_level = 0
|
--- @type number
|
||||||
|
local indent_level
|
||||||
local indent_str = ''
|
local indent_str = ''
|
||||||
if expandtab then
|
if expandtab then
|
||||||
while #indent_str < shiftwidth do
|
while #indent_str < shiftwidth do
|
||||||
|
|||||||
71
lua/u/extmark.lua
Normal file
71
lua/u/extmark.lua
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
local Pos = require 'u.pos'
|
||||||
|
|
||||||
|
---@class u.Extmark
|
||||||
|
---@field bufnr integer
|
||||||
|
---@field id integer
|
||||||
|
---@field nsid integer
|
||||||
|
local Extmark = {}
|
||||||
|
Extmark.__index = Extmark
|
||||||
|
|
||||||
|
--- @param bufnr integer
|
||||||
|
--- @param nsid integer
|
||||||
|
--- @param id integer
|
||||||
|
function Extmark.new(bufnr, nsid, id)
|
||||||
|
return setmetatable({
|
||||||
|
bufnr = bufnr,
|
||||||
|
nsid = nsid,
|
||||||
|
id = id,
|
||||||
|
}, Extmark)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param range u.Range
|
||||||
|
--- @param nsid integer
|
||||||
|
function Extmark.from_range(range, nsid)
|
||||||
|
local r = range:to_charwise()
|
||||||
|
local stop = r.stop or r.start
|
||||||
|
local end_row = stop.lnum - 1
|
||||||
|
local end_col = stop.col
|
||||||
|
if range.mode == 'V' then
|
||||||
|
end_row = end_row + 1
|
||||||
|
end_col = 0
|
||||||
|
end
|
||||||
|
local id = vim.api.nvim_buf_set_extmark(r.start.bufnr, nsid, r.start.lnum - 1, r.start.col - 1, {
|
||||||
|
right_gravity = false,
|
||||||
|
end_right_gravity = true,
|
||||||
|
end_row = end_row,
|
||||||
|
end_col = end_col,
|
||||||
|
})
|
||||||
|
return Extmark.new(r.start.bufnr, nsid, id)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Extmark:range()
|
||||||
|
local Range = require 'u.range'
|
||||||
|
|
||||||
|
local raw_extmark =
|
||||||
|
vim.api.nvim_buf_get_extmark_by_id(self.bufnr, self.nsid, self.id, { details = true })
|
||||||
|
local start_row0, start_col0, details = unpack(raw_extmark)
|
||||||
|
|
||||||
|
--- @type u.Pos
|
||||||
|
local start = Pos.from00(self.bufnr, start_row0, start_col0)
|
||||||
|
--- @type u.Pos?
|
||||||
|
local stop = details
|
||||||
|
and details.end_row
|
||||||
|
and details.end_col
|
||||||
|
and Pos.from01(self.bufnr, details.end_row, details.end_col)
|
||||||
|
|
||||||
|
local n_buf_lines = vim.api.nvim_buf_line_count(self.bufnr)
|
||||||
|
if stop and stop.lnum > n_buf_lines then
|
||||||
|
stop.lnum = n_buf_lines
|
||||||
|
stop = stop:eol()
|
||||||
|
end
|
||||||
|
if stop and stop.col == 0 then
|
||||||
|
stop.col = 1
|
||||||
|
stop = stop:next(-1)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Range.new(start, stop, 'v')
|
||||||
|
end
|
||||||
|
|
||||||
|
function Extmark:delete() vim.api.nvim_buf_del_extmark(self.bufnr, self.nsid, self.id) end
|
||||||
|
|
||||||
|
return Extmark
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
local LOG_ROOT = vim.fs.joinpath(vim.fn.stdpath 'cache', 'u.log')
|
||||||
|
|
||||||
--- @params name string
|
--- @params name string
|
||||||
function M.file_for_name(name) return vim.fs.joinpath(vim.fn.stdpath 'cache', 'u.log', name .. '.log.jsonl') end
|
function M.file_for_name(name) return vim.fs.joinpath(LOG_ROOT, name .. '.log.jsonl') end
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- Logger class
|
-- Logger class
|
||||||
@@ -9,7 +11,6 @@ function M.file_for_name(name) return vim.fs.joinpath(vim.fn.stdpath 'cache', 'u
|
|||||||
|
|
||||||
--- @class u.Logger
|
--- @class u.Logger
|
||||||
--- @field name string
|
--- @field name string
|
||||||
--- @field private fd number
|
|
||||||
local Logger = {}
|
local Logger = {}
|
||||||
Logger.__index = Logger
|
Logger.__index = Logger
|
||||||
M.Logger = Logger
|
M.Logger = Logger
|
||||||
@@ -18,10 +19,7 @@ M.Logger = Logger
|
|||||||
function Logger.new(name)
|
function Logger.new(name)
|
||||||
local file_path = M.file_for_name(name)
|
local file_path = M.file_for_name(name)
|
||||||
vim.fn.mkdir(vim.fs.dirname(file_path), 'p')
|
vim.fn.mkdir(vim.fs.dirname(file_path), 'p')
|
||||||
local self = setmetatable({
|
local self = setmetatable({ name = name }, Logger)
|
||||||
name = name,
|
|
||||||
fd = (vim.uv or vim.loop).fs_open(file_path, 'a', tonumber('644', 8)),
|
|
||||||
}, Logger)
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -30,7 +28,12 @@ end
|
|||||||
function Logger:write(level, ...)
|
function Logger:write(level, ...)
|
||||||
local data = { ... }
|
local data = { ... }
|
||||||
if #data == 1 then data = data[1] end
|
if #data == 1 then data = data[1] end
|
||||||
(vim.uv or vim.loop).fs_write(self.fd, vim.json.encode { ts = os.date(), level = level, data = data } .. '\n')
|
local f = assert(io.open(M.file_for_name(self.name), 'a'), 'could not open file')
|
||||||
|
assert(
|
||||||
|
f:write(vim.json.encode { ts = os.date(), level = level, data = data } .. '\n'),
|
||||||
|
'could not write to file'
|
||||||
|
)
|
||||||
|
f:close()
|
||||||
end
|
end
|
||||||
|
|
||||||
function Logger:trace(...) self:write('INFO', ...) end
|
function Logger:trace(...) self:write('INFO', ...) end
|
||||||
@@ -59,6 +62,12 @@ function M.setup()
|
|||||||
vim.cmd.terminal('tail -f "' .. log_file_path .. '"')
|
vim.cmd.terminal('tail -f "' .. log_file_path .. '"')
|
||||||
vim.cmd.startinsert()
|
vim.cmd.startinsert()
|
||||||
end, { nargs = '*' })
|
end, { nargs = '*' })
|
||||||
|
|
||||||
|
vim.api.nvim_create_user_command(
|
||||||
|
'Logroot',
|
||||||
|
function() vim.api.nvim_echo({ { LOG_ROOT } }, false, {}) end,
|
||||||
|
{}
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ local __U__OpKeymapOpFunc_rhs = nil
|
|||||||
|
|
||||||
--- This is the global utility function used for operatorfunc
|
--- This is the global utility function used for operatorfunc
|
||||||
--- in opkeymap
|
--- in opkeymap
|
||||||
--- @type nil|fun(range: u.Range): fun():any|nil
|
|
||||||
--- @param ty 'line'|'char'|'block'
|
--- @param ty 'line'|'char'|'block'
|
||||||
-- selene: allow(unused_variable)
|
-- selene: allow(unused_variable)
|
||||||
function __U__OpKeymapOpFunc(ty)
|
function _G.__U__OpKeymapOpFunc(ty)
|
||||||
if __U__OpKeymapOpFunc_rhs ~= nil then
|
if __U__OpKeymapOpFunc_rhs ~= nil then
|
||||||
local range = Range.from_op_func(ty)
|
local range = Range.from_op_func(ty)
|
||||||
__U__OpKeymapOpFunc_rhs(range)
|
__U__OpKeymapOpFunc_rhs(range)
|
||||||
|
|||||||
112
lua/u/pos.lua
112
lua/u/pos.lua
@@ -2,16 +2,27 @@ local MAX_COL = vim.v.maxcol
|
|||||||
|
|
||||||
--- @param bufnr number
|
--- @param bufnr number
|
||||||
--- @param lnum number 1-based
|
--- @param lnum number 1-based
|
||||||
local function line_text(bufnr, lnum) return vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, false)[1] end
|
local function line_text(bufnr, lnum)
|
||||||
|
return vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, false)[1]
|
||||||
|
end
|
||||||
|
|
||||||
--- @class u.Pos
|
--- @class u.Pos
|
||||||
--- @field bufnr number buffer number
|
--- @field bufnr integer buffer number
|
||||||
--- @field lnum number 1-based line index
|
--- @field lnum integer 1-based line index
|
||||||
--- @field col number 1-based column index
|
--- @field col integer 1-based column index
|
||||||
--- @field off number
|
--- @field off number
|
||||||
local Pos = {}
|
local Pos = {}
|
||||||
|
Pos.__index = Pos
|
||||||
Pos.MAX_COL = MAX_COL
|
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 bufnr? number
|
||||||
--- @param lnum number 1-based
|
--- @param lnum number 1-based
|
||||||
--- @param col number 1-based
|
--- @param col number 1-based
|
||||||
@@ -20,56 +31,68 @@ Pos.MAX_COL = MAX_COL
|
|||||||
function Pos.new(bufnr, lnum, col, off)
|
function Pos.new(bufnr, lnum, col, off)
|
||||||
if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
|
if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
|
||||||
if off == nil then off = 0 end
|
if off == nil then off = 0 end
|
||||||
local pos = {
|
--- @type u.Pos
|
||||||
|
return setmetatable({
|
||||||
bufnr = bufnr,
|
bufnr = bufnr,
|
||||||
lnum = lnum,
|
lnum = lnum,
|
||||||
col = col,
|
col = col,
|
||||||
off = off,
|
off = off,
|
||||||
}
|
}, 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
|
end
|
||||||
|
|
||||||
|
--- @param bufnr? number
|
||||||
|
--- @param lnum0 number 1-based
|
||||||
|
--- @param col0 number 1-based
|
||||||
|
--- @param off? number
|
||||||
|
function Pos.from00(bufnr, lnum0, col0, off) return Pos.new(bufnr, lnum0 + 1, col0 + 1, off) end
|
||||||
|
|
||||||
|
--- @param bufnr? number
|
||||||
|
--- @param lnum0 number 1-based
|
||||||
|
--- @param col1 number 1-based
|
||||||
|
--- @param off? number
|
||||||
|
function Pos.from01(bufnr, lnum0, col1, off) return Pos.new(bufnr, lnum0 + 1, col1, off) end
|
||||||
|
|
||||||
|
--- @param bufnr? number
|
||||||
|
--- @param lnum1 number 1-based
|
||||||
|
--- @param col0 number 1-based
|
||||||
|
--- @param off? number
|
||||||
|
function Pos.from10(bufnr, lnum1, col0, off) return Pos.new(bufnr, lnum1, col0 + 1, off) end
|
||||||
|
|
||||||
function Pos.invalid() return Pos.new(0, 0, 0, 0) end
|
function Pos.invalid() return Pos.new(0, 0, 0, 0) end
|
||||||
|
|
||||||
function Pos.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.__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.__le(a, b) return a < b or a == b end
|
||||||
function Pos.__eq(a, b) 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.__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
|
||||||
|
end
|
||||||
function Pos.__add(x, y)
|
function Pos.__add(x, y)
|
||||||
if type(x) == 'number' then
|
if type(x) == 'number' then
|
||||||
x, y = y, x
|
x, y = y, x
|
||||||
end
|
end
|
||||||
if not Pos.is(x) or type(y) ~= 'number' then return nil end
|
if getmetatable(x) ~= Pos or type(y) ~= 'number' then return nil end
|
||||||
return x:next(y)
|
return x:next(y)
|
||||||
end
|
end
|
||||||
function Pos.__sub(x, y)
|
function Pos.__sub(x, y)
|
||||||
if type(x) == 'number' then
|
if type(x) == 'number' then
|
||||||
x, y = y, x
|
x, y = y, x
|
||||||
end
|
end
|
||||||
if not Pos.is(x) or type(y) ~= 'number' then return nil end
|
if getmetatable(x) ~= Pos or type(y) ~= 'number' then return nil end
|
||||||
return x:next(-y)
|
return x:next(-y)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- @param bufnr number
|
||||||
|
--- @param lnum number
|
||||||
|
function Pos.from_eol(bufnr, lnum)
|
||||||
|
if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
|
||||||
|
local pos = Pos.new(bufnr, lnum, 0)
|
||||||
|
pos.col = pos:line():len()
|
||||||
|
return pos
|
||||||
|
end
|
||||||
|
|
||||||
--- @param name string
|
--- @param name string
|
||||||
--- @return u.Pos
|
--- @return u.Pos
|
||||||
function Pos.from_pos(name)
|
function Pos.from_pos(name)
|
||||||
@@ -77,7 +100,7 @@ function Pos.from_pos(name)
|
|||||||
return Pos.new(p[1], p[2], p[3], p[4])
|
return Pos.new(p[1], p[2], p[3], p[4])
|
||||||
end
|
end
|
||||||
|
|
||||||
function Pos:is_invalid() return self.bufnr == 0 and self.lnum == 0 and self.col == 0 and self.off == 0 end
|
function Pos:is_invalid() return self.lnum == 0 and self.col == 0 and self.off == 0 end
|
||||||
|
|
||||||
function Pos:clone() return Pos.new(self.bufnr, self.lnum, self.col, self.off) end
|
function Pos:clone() return Pos.new(self.bufnr, self.lnum, self.col, self.off) end
|
||||||
|
|
||||||
@@ -99,9 +122,16 @@ end
|
|||||||
|
|
||||||
function Pos:as_vim() return { self.bufnr, self.lnum, self.col, self.off } end
|
function Pos:as_vim() return { self.bufnr, self.lnum, self.col, self.off } end
|
||||||
|
|
||||||
|
function Pos:eol() return Pos.from_eol(self.bufnr, self.lnum) end
|
||||||
|
|
||||||
--- @param pos string
|
--- @param pos string
|
||||||
function Pos:save_to_pos(pos) vim.fn.setpos(pos, { self.bufnr, self.lnum, self.col, self.off }) end
|
function Pos:save_to_pos(pos) vim.fn.setpos(pos, { self.bufnr, self.lnum, self.col, self.off }) end
|
||||||
|
|
||||||
|
--- @param winnr? integer
|
||||||
|
function Pos:save_to_cursor(winnr)
|
||||||
|
vim.api.nvim_win_set_cursor(winnr or 0, { self.lnum, self.col - 1 })
|
||||||
|
end
|
||||||
|
|
||||||
--- @param mark string
|
--- @param mark string
|
||||||
function Pos:save_to_mark(mark)
|
function Pos:save_to_mark(mark)
|
||||||
local p = self:as_real()
|
local p = self:as_real()
|
||||||
@@ -202,7 +232,9 @@ end
|
|||||||
--- @return u.Pos|nil
|
--- @return u.Pos|nil
|
||||||
function Pos:find_match(max_chars, invocations)
|
function Pos:find_match(max_chars, invocations)
|
||||||
if invocations == nil then invocations = {} end
|
if invocations == nil then invocations = {} end
|
||||||
if vim.tbl_contains(invocations, function(p) return self == p end, { predicate = true }) then return nil end
|
if vim.tbl_contains(invocations, function(p) return self == p end, { predicate = true }) then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
table.insert(invocations, self)
|
table.insert(invocations, self)
|
||||||
|
|
||||||
local openers = { '{', '[', '(', '<' }
|
local openers = { '{', '[', '(', '<' }
|
||||||
@@ -212,7 +244,10 @@ function Pos:find_match(max_chars, invocations)
|
|||||||
local is_closer = vim.tbl_contains(closers, c)
|
local is_closer = vim.tbl_contains(closers, c)
|
||||||
if not is_opener and not is_closer then return nil end
|
if not is_opener and not is_closer then return nil end
|
||||||
|
|
||||||
local i, _ = vim.iter(is_opener and openers or closers):enumerate():find(function(_, c2) return c == c2 end)
|
local i, _ = vim
|
||||||
|
.iter(is_opener and openers or closers)
|
||||||
|
:enumerate()
|
||||||
|
:find(function(_, c2) return c == c2 end)
|
||||||
-- Store the character we will be looking for:
|
-- Store the character we will be looking for:
|
||||||
local c_match = (is_opener and closers or openers)[i]
|
local c_match = (is_opener and closers or openers)[i]
|
||||||
|
|
||||||
@@ -255,7 +290,14 @@ end
|
|||||||
--- @param lines string|string[]
|
--- @param lines string|string[]
|
||||||
function Pos:insert_before(lines)
|
function Pos:insert_before(lines)
|
||||||
if type(lines) == 'string' then lines = vim.split(lines, '\n') end
|
if type(lines) == 'string' then lines = vim.split(lines, '\n') end
|
||||||
vim.api.nvim_buf_set_text(self.bufnr, self.lnum - 1, self.col - 1, self.lnum - 1, self.col - 1, lines)
|
vim.api.nvim_buf_set_text(
|
||||||
|
self.bufnr,
|
||||||
|
self.lnum - 1,
|
||||||
|
self.col - 1,
|
||||||
|
self.lnum - 1,
|
||||||
|
self.col - 1,
|
||||||
|
lines
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
return Pos
|
return Pos
|
||||||
|
|||||||
499
lua/u/range.lua
499
lua/u/range.lua
@@ -1,17 +1,41 @@
|
|||||||
|
local Extmark = require 'u.extmark'
|
||||||
local Pos = require 'u.pos'
|
local Pos = require 'u.pos'
|
||||||
|
|
||||||
local orig_on_yank = (vim.hl or vim.highlight).on_yank
|
local ESC = vim.api.nvim_replace_termcodes('<Esc>', true, false, true)
|
||||||
local on_yank_enabled = true;
|
local NS = vim.api.nvim_create_namespace 'u.range'
|
||||||
((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
|
--- @class u.Range
|
||||||
--- @field start u.Pos
|
--- @field start u.Pos
|
||||||
--- @field stop u.Pos|nil
|
--- @field stop u.Pos|nil
|
||||||
--- @field mode 'v'|'V'
|
--- @field mode 'v'|'V'
|
||||||
local Range = {}
|
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 start u.Pos
|
||||||
--- @param stop u.Pos|nil
|
--- @param stop u.Pos|nil
|
||||||
@@ -23,29 +47,24 @@ function Range.new(start, stop, mode)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local r = { start = start, stop = stop, mode = mode or 'v' }
|
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
|
|
||||||
|
|
||||||
local _1 = posstr(r.start)
|
setmetatable(r, Range)
|
||||||
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
|
return r
|
||||||
end
|
end
|
||||||
|
|
||||||
function Range.is(x)
|
--- @param ranges (u.Range|nil)[]
|
||||||
local mt = getmetatable(x)
|
function Range.smallest(ranges)
|
||||||
return mt and mt.__index == Range
|
--- @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
|
end
|
||||||
|
|
||||||
--- @param lpos string
|
--- @param lpos string
|
||||||
@@ -92,81 +111,111 @@ function Range.from_lines(bufnr, start_line, stop_line)
|
|||||||
return Range.new(Pos.new(bufnr, start_line, 1), Pos.new(bufnr, stop_line, Pos.MAX_COL), 'V')
|
return Range.new(Pos.new(bufnr, start_line, 1), Pos.new(bufnr, stop_line, Pos.MAX_COL), 'V')
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param text_obj string
|
--- @param motion string
|
||||||
--- @param opts? { bufnr?: number; contains_cursor?: boolean; pos?: u.Pos, user_defined?: boolean }
|
--- @param opts? { bufnr?: number, contains_cursor?: boolean, pos?: u.Pos, user_defined?: boolean }
|
||||||
--- @return u.Range|nil
|
--- @return u.Range|nil
|
||||||
function Range.from_motion(text_obj, opts)
|
function Range.from_motion(motion, opts)
|
||||||
|
-- Options handling:
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
if opts.bufnr == nil then opts.bufnr = vim.api.nvim_get_current_buf() end
|
if opts.bufnr == nil or opts.bufnr == 0 then opts.bufnr = vim.api.nvim_get_current_buf() end
|
||||||
if opts.contains_cursor == nil then opts.contains_cursor = false end
|
if opts.contains_cursor == nil then opts.contains_cursor = false end
|
||||||
if opts.user_defined == nil then opts.user_defined = false end
|
if opts.user_defined == nil then opts.user_defined = false end
|
||||||
|
|
||||||
--- @type "a" | "i"
|
-- Extract some information from the motion:
|
||||||
local selection_type = text_obj:sub(1, 1)
|
--- @type 'a'|'i', string
|
||||||
local obj_type = text_obj:sub(#text_obj, #text_obj)
|
local scope, motion_rest = motion:sub(1, 1), motion:sub(2)
|
||||||
local is_quote = vim.tbl_contains({ "'", '"', '`' }, obj_type)
|
local is_txtobj = scope == 'a' or scope == 'i'
|
||||||
local cursor = Pos.from_pos '.'
|
local is_quote_txtobj = is_txtobj and vim.tbl_contains({ "'", '"', '`' }, motion_rest)
|
||||||
|
|
||||||
--- @type u.Pos
|
-- Capture the original state of the buffer for restoration later.
|
||||||
local start
|
local original_state = {
|
||||||
--- @type u.Pos
|
winview = vim.fn.winsaveview(),
|
||||||
local stop
|
regquote = vim.fn.getreg '"',
|
||||||
|
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()
|
vim.api.nvim_buf_call(opts.bufnr, function()
|
||||||
local original_state = {
|
|
||||||
winview = vim.fn.winsaveview(),
|
|
||||||
regquote = vim.fn.getreg '"',
|
|
||||||
posdot = vim.fn.getpos '.',
|
|
||||||
poslb = vim.fn.getpos "'[",
|
|
||||||
posrb = vim.fn.getpos "']",
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.pos ~= nil then opts.pos:save_to_pos '.' end
|
if opts.pos ~= nil then opts.pos:save_to_pos '.' end
|
||||||
|
|
||||||
Pos.invalid():save_to_pos "'["
|
_G.Range__from_motion_opfunc = function(ty)
|
||||||
Pos.invalid():save_to_pos "']"
|
_G.Range__from_motion_opfunc_captured_range = Range.from_op_func(ty)
|
||||||
|
end
|
||||||
local prev_on_yank_enabled = on_yank_enabled
|
local old_eventignore = vim.o.eventignore
|
||||||
on_yank_enabled = false
|
vim.o.eventignore = 'all'
|
||||||
|
vim.go.operatorfunc = 'v:lua.Range__from_motion_opfunc'
|
||||||
vim.cmd {
|
vim.cmd {
|
||||||
cmd = 'normal',
|
cmd = 'normal',
|
||||||
bang = not opts.user_defined,
|
bang = not opts.user_defined,
|
||||||
args = { '""y' .. text_obj },
|
args = { ESC .. 'g@' .. motion },
|
||||||
mods = { silent = true },
|
mods = { silent = true },
|
||||||
}
|
}
|
||||||
on_yank_enabled = prev_on_yank_enabled
|
vim.o.eventignore = old_eventignore
|
||||||
|
|
||||||
start = Pos.from_pos "'["
|
|
||||||
stop = Pos.from_pos "']"
|
|
||||||
|
|
||||||
-- Restore original state:
|
|
||||||
vim.fn.winrestview(original_state.winview)
|
|
||||||
vim.fn.setreg('"', original_state.regquote)
|
|
||||||
vim.fn.setpos('.', original_state.posdot)
|
|
||||||
vim.fn.setpos("'[", original_state.poslb)
|
|
||||||
vim.fn.setpos("']", original_state.posrb)
|
|
||||||
|
|
||||||
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 and selection_type == 'i' and stop:char() == obj_type)
|
|
||||||
-- *Sigh*, this also sometimes happens for `it` as well.
|
|
||||||
or (text_obj == 'it' and stop:char() == '<')
|
|
||||||
then
|
|
||||||
stop = stop:next(-1) or stop
|
|
||||||
end
|
|
||||||
end)
|
end)
|
||||||
|
local captured_range = _G.Range__from_motion_opfunc_captured_range
|
||||||
|
|
||||||
if start == stop and start:is_invalid() then return nil end
|
-- Restore original state:
|
||||||
if opts.contains_cursor and not Range.new(start, stop):contains(cursor) then return nil end
|
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 is_quote and selection_type == 'a' then
|
if not captured_range then return nil end
|
||||||
start = start:find_next(1, obj_type) or start
|
|
||||||
stop = stop:find_next(-1, obj_type) or stop
|
-- 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)
|
||||||
|
-- *Sigh*, this also sometimes happens for `it` as well.
|
||||||
|
or (motion == 'it' and captured_range.stop:char() == '<')
|
||||||
|
then
|
||||||
|
captured_range.stop = captured_range.stop:next(-1) or captured_range.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
|
||||||
end
|
end
|
||||||
|
|
||||||
return Range.new(start, stop)
|
if
|
||||||
|
opts.contains_cursor and not captured_range:contains(Pos.new(unpack(original_state.cursor)))
|
||||||
|
then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return captured_range
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param opts? { contains_cursor?: boolean }
|
||||||
|
function Range.from_tsquery_caps(bufnr, query, opts)
|
||||||
|
opts = opts or { contains_cursor = true }
|
||||||
|
|
||||||
|
local ranges = Range.from_buf_text(bufnr):tsquery(query)
|
||||||
|
if not ranges then return end
|
||||||
|
if not opts.contains_cursor then return ranges end
|
||||||
|
|
||||||
|
local cursor = Pos.from_pos '.'
|
||||||
|
return vim.tbl_map(function(cap_ranges)
|
||||||
|
return vim
|
||||||
|
.iter(cap_ranges)
|
||||||
|
:filter(
|
||||||
|
--- @param r u.Range
|
||||||
|
function(r) return r:contains(cursor) end
|
||||||
|
)
|
||||||
|
:totable()
|
||||||
|
end, ranges)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Get range information from the currently selected visual text.
|
--- Get range information from the currently selected visual text.
|
||||||
@@ -196,22 +245,24 @@ end
|
|||||||
--- @param args unknown
|
--- @param args unknown
|
||||||
--- @return u.Range|nil
|
--- @return u.Range|nil
|
||||||
function Range.from_cmd_args(args)
|
function Range.from_cmd_args(args)
|
||||||
--- @type 'v'|'V'
|
if args.range == 0 then return nil end
|
||||||
local mode
|
|
||||||
--- @type nil|u.Pos
|
local bufnr = vim.api.nvim_get_current_buf()
|
||||||
local start
|
if args.range == 1 then
|
||||||
local stop
|
return Range.new(Pos.new(bufnr, args.line1, 1), Pos.new(bufnr, args.line1, Pos.MAX_COL), 'V')
|
||||||
if args.range == 0 then
|
end
|
||||||
return nil
|
|
||||||
else
|
local is_visual = vim.fn.histget('cmd', -1):sub(1, 5) == [['<,'>]]
|
||||||
start = Pos.from_pos "'<"
|
--- @type 'v'|'V'
|
||||||
stop = Pos.from_pos "'>"
|
local mode = is_visual and vim.fn.visualmode() or 'V'
|
||||||
mode = stop:is_col_max() and 'V' or 'v'
|
|
||||||
|
if is_visual then
|
||||||
|
return Range.new(Pos.from_pos "'<", Pos.from_pos "'>", mode)
|
||||||
|
else
|
||||||
|
return Range.new(Pos.new(bufnr, args.line1, 1), Pos.new(bufnr, args.line2, Pos.MAX_COL), mode)
|
||||||
end
|
end
|
||||||
return Range.new(start, stop, mode)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---
|
|
||||||
function Range.find_nearest_brackets()
|
function Range.find_nearest_brackets()
|
||||||
return Range.smallest {
|
return Range.smallest {
|
||||||
Range.from_motion('a<', { contains_cursor = true }),
|
Range.from_motion('a<', { contains_cursor = true }),
|
||||||
@@ -229,26 +280,15 @@ function Range.find_nearest_quotes()
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param ranges (u.Range|nil)[]
|
--------------------------------------------------------------------------------
|
||||||
function Range.smallest(ranges)
|
-- Structural utilities:
|
||||||
--- @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
|
function Range:clone()
|
||||||
local smallest = ranges[1]
|
return Range.new(self.start:clone(), self.stop ~= nil and self.stop:clone() or nil, self.mode)
|
||||||
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
|
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()
|
function Range:to_linewise()
|
||||||
local r = self:clone()
|
local r = self:clone()
|
||||||
@@ -260,7 +300,142 @@ function Range:to_linewise()
|
|||||||
return r
|
return r
|
||||||
end
|
end
|
||||||
|
|
||||||
function Range:is_empty() return self.stop == nil end
|
function Range:to_charwise()
|
||||||
|
local r = self:clone()
|
||||||
|
r.mode = 'v'
|
||||||
|
if r.stop:is_col_max() then r.stop = r.stop:as_real() end
|
||||||
|
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)
|
||||||
|
self.start:save_to_pos(left);
|
||||||
|
(self:is_empty() and self.start or self.stop):save_to_pos(right)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param left string
|
||||||
|
--- @param right string
|
||||||
|
function Range:save_to_marks(left, right)
|
||||||
|
self.start:save_to_mark(left);
|
||||||
|
(self:is_empty() and self.start or self.stop):save_to_mark(right)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Range:save_to_extmark() return Extmark.from_range(self, NS) 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
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Text access/manipulation utilities:
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
--- @param query string
|
||||||
|
function Range:tsquery(query)
|
||||||
|
local bufnr = self.start.bufnr
|
||||||
|
|
||||||
|
local lang = vim.treesitter.language.get_lang(vim.bo[bufnr].filetype)
|
||||||
|
if lang == nil then return end
|
||||||
|
local parser = vim.treesitter.get_parser(bufnr, lang)
|
||||||
|
if parser == nil then return end
|
||||||
|
local tree = parser:parse()[1]
|
||||||
|
if tree == nil then return end
|
||||||
|
|
||||||
|
local root = tree:root()
|
||||||
|
local q = vim.treesitter.query.parse(lang, query)
|
||||||
|
--- @type table<string, u.Range[]>
|
||||||
|
local ranges = {}
|
||||||
|
for id, match, _meta in
|
||||||
|
q:iter_captures(root, bufnr, self.start.lnum - 1, (self.stop or self.start).lnum)
|
||||||
|
do
|
||||||
|
local start_row0, start_col0, stop_row0, stop_col0 = match:range()
|
||||||
|
local range = Range.new(
|
||||||
|
Pos.new(bufnr, start_row0 + 1, start_col0 + 1),
|
||||||
|
Pos.new(bufnr, stop_row0 + 1, stop_col0),
|
||||||
|
'v'
|
||||||
|
)
|
||||||
|
if range.stop.lnum > vim.api.nvim_buf_line_count(bufnr) then
|
||||||
|
range.stop = range.stop:must_next(-1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local capture_name = q.captures[id]
|
||||||
|
if not ranges[capture_name] then ranges[capture_name] = {} end
|
||||||
|
if self:contains(range) then table.insert(ranges[capture_name], range) end
|
||||||
|
end
|
||||||
|
|
||||||
|
return ranges
|
||||||
|
end
|
||||||
|
|
||||||
|
function Range:length()
|
||||||
|
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:trim_start()
|
function Range:trim_start()
|
||||||
if self:is_empty() then return end
|
if self:is_empty() then return end
|
||||||
@@ -286,8 +461,46 @@ function Range:trim_stop()
|
|||||||
return r
|
return r
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param p u.Pos
|
--- @param i number 1-based
|
||||||
function Range:contains(p) return not self:is_empty() and p >= self.start and p <= self.stop end
|
--- @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
|
||||||
|
|
||||||
--- @return string[]
|
--- @return string[]
|
||||||
function Range:lines()
|
function Range:lines()
|
||||||
@@ -298,17 +511,15 @@ end
|
|||||||
--- @return string
|
--- @return string
|
||||||
function Range:text() return vim.fn.join(self:lines(), '\n') end
|
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
|
--- @param l number
|
||||||
--- @return { line: string; idx0: { start: number; stop: number; }; lnum: number; range: fun():u.Range; text: fun():string }|nil
|
-- luacheck: ignore
|
||||||
|
--- @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
|
||||||
|
|
||||||
local line_indices = vim.fn.getregionpos(self.start:as_vim(), self.stop:as_vim(), { type = self.mode })
|
local line_indices =
|
||||||
|
vim.fn.getregionpos(self.start:as_vim(), self.stop:as_vim(), { type = self.mode })
|
||||||
local line_bounds = line_indices[l]
|
local line_bounds = line_indices[l]
|
||||||
|
|
||||||
local start = Pos.new(unpack(line_bounds[1]))
|
local start = Pos.new(unpack(line_bounds[1]))
|
||||||
@@ -327,7 +538,9 @@ function Range:replace(replacement)
|
|||||||
local function update_stop_non_linewise()
|
local function update_stop_non_linewise()
|
||||||
local new_last_line_num = self.start.lnum + #replacement - 1
|
local new_last_line_num = self.start.lnum + #replacement - 1
|
||||||
local new_last_col = #(replacement[#replacement] or '')
|
local new_last_col = #(replacement[#replacement] or '')
|
||||||
if new_last_line_num == self.start.lnum then new_last_col = new_last_col + self.start.col - 1 end
|
if new_last_line_num == self.start.lnum then
|
||||||
|
new_last_col = new_last_col + self.start.col - 1
|
||||||
|
end
|
||||||
self.stop = Pos.new(bufnr, new_last_line_num, new_last_col)
|
self.stop = Pos.new(bufnr, new_last_line_num, new_last_col)
|
||||||
end
|
end
|
||||||
local function update_stop_linewise()
|
local function update_stop_linewise()
|
||||||
@@ -397,48 +610,12 @@ end
|
|||||||
--- @param amount number
|
--- @param amount number
|
||||||
function Range:must_shrink(amount)
|
function Range:must_shrink(amount)
|
||||||
local shrunk = self:shrink(amount)
|
local shrunk = self:shrink(amount)
|
||||||
if shrunk == nil or shrunk:is_empty() then error 'error in Range:must_shrink: Range:shrink() returned nil' end
|
if shrunk == nil or shrunk:is_empty() then
|
||||||
|
error 'error in Range:must_shrink: Range:shrink() returned nil'
|
||||||
|
end
|
||||||
return shrunk
|
return shrunk
|
||||||
end
|
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 group string
|
||||||
--- @param opts? { timeout?: number, priority?: number, on_macro?: boolean }
|
--- @param opts? { timeout?: number, priority?: number, on_macro?: boolean }
|
||||||
function Range:highlight(group, opts)
|
function Range:highlight(group, opts)
|
||||||
@@ -452,8 +629,8 @@ function Range:highlight(group, opts)
|
|||||||
|
|
||||||
local ns = vim.api.nvim_create_namespace ''
|
local ns = vim.api.nvim_create_namespace ''
|
||||||
|
|
||||||
local winview = vim.fn.winsaveview();
|
local winview = vim.fn.winsaveview()
|
||||||
(vim.hl or vim.highlight).range(
|
vim.hl.range(
|
||||||
self.start.bufnr,
|
self.start.bufnr,
|
||||||
ns,
|
ns,
|
||||||
group,
|
group,
|
||||||
|
|||||||
1023
lua/u/renderer.lua
1023
lua/u/renderer.lua
File diff suppressed because it is too large
Load Diff
@@ -6,19 +6,19 @@ M.debug = false
|
|||||||
-- class Signal
|
-- class Signal
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
--- @class u.Signal
|
--- @class u.Signal<T>
|
||||||
--- @field name? string
|
--- @field name? string
|
||||||
--- @field private changing boolean
|
--- @field private changing boolean
|
||||||
--- @field private value any
|
--- @field private value T
|
||||||
--- @field private subscribers table<function, boolean>
|
--- @field private subscribers table<function, boolean>
|
||||||
--- @field private on_dispose_callbacks function[]
|
--- @field private on_dispose_callbacks function[]
|
||||||
local Signal = {}
|
local Signal = {}
|
||||||
M.Signal = Signal
|
M.Signal = Signal
|
||||||
Signal.__index = Signal
|
Signal.__index = Signal
|
||||||
|
|
||||||
--- @param value any
|
--- @param value `T`
|
||||||
--- @param name? string
|
--- @param name? string
|
||||||
--- @return u.Signal
|
--- @return u.Signal<T>
|
||||||
function Signal:new(value, name)
|
function Signal:new(value, name)
|
||||||
local obj = setmetatable({
|
local obj = setmetatable({
|
||||||
name = name,
|
name = name,
|
||||||
@@ -30,14 +30,17 @@ function Signal:new(value, name)
|
|||||||
return obj
|
return obj
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param value any
|
--- @param value T
|
||||||
function Signal:set(value)
|
function Signal:set(value)
|
||||||
self.value = value
|
self.value = value
|
||||||
|
|
||||||
-- We don't handle cyclic updates:
|
-- We don't handle cyclic updates:
|
||||||
if self.changing then
|
if self.changing then
|
||||||
if M.debug then
|
if M.debug then
|
||||||
vim.notify('circular dependency detected' .. (self.name and (' in ' .. self.name) or ''), vim.log.levels.WARN)
|
vim.notify(
|
||||||
|
'circular dependency detected' .. (self.name and (' in ' .. self.name) or ''),
|
||||||
|
vim.log.levels.WARN
|
||||||
|
)
|
||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -64,11 +67,12 @@ function Signal:set(value)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- @param value T
|
||||||
function Signal:schedule_set(value)
|
function Signal:schedule_set(value)
|
||||||
vim.schedule(function() self:set(value) end)
|
vim.schedule(function() self:set(value) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @return any
|
--- @return T
|
||||||
function Signal:get()
|
function Signal:get()
|
||||||
local ctx = M.ExecutionContext.current()
|
local ctx = M.ExecutionContext.current()
|
||||||
if ctx then ctx:track(self) end
|
if ctx then ctx:track(self) end
|
||||||
@@ -82,8 +86,8 @@ function Signal:update(fn) self:set(fn(self.value)) end
|
|||||||
function Signal:schedule_update(fn) self:schedule_set(fn(self.value)) end
|
function Signal:schedule_update(fn) self:schedule_set(fn(self.value)) end
|
||||||
|
|
||||||
--- @generic U
|
--- @generic U
|
||||||
--- @param fn fun(value: T): U
|
--- @param fn fun(value: T): `U`
|
||||||
--- @return u.Signal --<U>
|
--- @return u.Signal<U>
|
||||||
function Signal:map(fn)
|
function Signal:map(fn)
|
||||||
local mapped_signal = M.create_memo(function()
|
local mapped_signal = M.create_memo(function()
|
||||||
local value = self:get()
|
local value = self:get()
|
||||||
@@ -92,13 +96,13 @@ function Signal:map(fn)
|
|||||||
return mapped_signal
|
return mapped_signal
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @return u.Signal
|
--- @return u.Signal<T>
|
||||||
function Signal:clone()
|
function Signal:clone()
|
||||||
return self:map(function(x) return x end)
|
return self:map(function(x) return x end)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param fn fun(value: T): boolean
|
--- @param fn fun(value: T): boolean
|
||||||
--- @return u.Signal -- <T>
|
--- @return u.Signal<T>
|
||||||
function Signal:filter(fn)
|
function Signal:filter(fn)
|
||||||
local filtered_signal = M.create_signal(nil, self.name and self.name .. ':filtered' or nil)
|
local filtered_signal = M.create_signal(nil, self.name and self.name .. ':filtered' or nil)
|
||||||
local unsubscribe_from_self = self:subscribe(function(value)
|
local unsubscribe_from_self = self:subscribe(function(value)
|
||||||
@@ -109,10 +113,10 @@ function Signal:filter(fn)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- @param ms number
|
--- @param ms number
|
||||||
--- @return u.Signal -- <T>
|
--- @return u.Signal<T>
|
||||||
function Signal:debounce(ms)
|
function Signal:debounce(ms)
|
||||||
local function set_timeout(timeout, callback)
|
local function set_timeout(timeout, callback)
|
||||||
local timer = (vim.uv or vim.loop).new_timer()
|
local timer = assert((vim.uv or vim.loop).new_timer(), 'could not create new timer')
|
||||||
timer:start(timeout, 0, function()
|
timer:start(timeout, 0, function()
|
||||||
timer:stop()
|
timer:stop()
|
||||||
timer:close()
|
timer:close()
|
||||||
@@ -124,7 +128,7 @@ function Signal:debounce(ms)
|
|||||||
local filtered = M.create_signal(self.value, self.name and self.name .. ':debounced' or nil)
|
local filtered = M.create_signal(self.value, self.name and self.name .. ':debounced' or nil)
|
||||||
|
|
||||||
--- @diagnostic disable-next-line: undefined-doc-name
|
--- @diagnostic disable-next-line: undefined-doc-name
|
||||||
--- @type { queued: { value: T, ts: number }[]; timer?: uv_timer_t; }
|
--- @type { queued: { value: T, ts: number }[], timer?: uv.uv_timer_t }
|
||||||
local state = { queued = {}, timer = nil }
|
local state = { queued = {}, timer = nil }
|
||||||
local function clear_timeout()
|
local function clear_timeout()
|
||||||
if state.timer == nil then return end
|
if state.timer == nil then return end
|
||||||
@@ -143,7 +147,10 @@ function Signal:debounce(ms)
|
|||||||
local now_ms = (vim.uv or vim.loop).hrtime() / 1e6
|
local now_ms = (vim.uv or vim.loop).hrtime() / 1e6
|
||||||
|
|
||||||
-- If there is anything older than `ms` in our queue, emit it:
|
-- If there is anything older than `ms` in our queue, emit it:
|
||||||
local older_than_ms = vim.iter(state.queued):filter(function(item) return now_ms - item.ts > ms end):totable()
|
local older_than_ms = vim
|
||||||
|
.iter(state.queued)
|
||||||
|
:filter(function(item) return now_ms - item.ts > ms end)
|
||||||
|
:totable()
|
||||||
local last_older_than_ms = older_than_ms[#older_than_ms]
|
local last_older_than_ms = older_than_ms[#older_than_ms]
|
||||||
if last_older_than_ms then
|
if last_older_than_ms then
|
||||||
filtered:set(last_older_than_ms.value)
|
filtered:set(last_older_than_ms.value)
|
||||||
@@ -196,7 +203,8 @@ end
|
|||||||
-- class ExecutionContext
|
-- class ExecutionContext
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
CURRENT_CONTEXT = nil
|
--- @type u.ExecutionContext|nil
|
||||||
|
local CURRENT_CONTEXT = nil
|
||||||
|
|
||||||
--- @class u.ExecutionContext
|
--- @class u.ExecutionContext
|
||||||
--- @field signals table<u.Signal, boolean>
|
--- @field signals table<u.Signal, boolean>
|
||||||
@@ -205,7 +213,7 @@ M.ExecutionContext = ExecutionContext
|
|||||||
ExecutionContext.__index = ExecutionContext
|
ExecutionContext.__index = ExecutionContext
|
||||||
|
|
||||||
--- @return u.ExecutionContext
|
--- @return u.ExecutionContext
|
||||||
function ExecutionContext:new()
|
function ExecutionContext.new()
|
||||||
return setmetatable({
|
return setmetatable({
|
||||||
signals = {},
|
signals = {},
|
||||||
subscribers = {},
|
subscribers = {},
|
||||||
@@ -216,7 +224,7 @@ function ExecutionContext.current() return CURRENT_CONTEXT end
|
|||||||
|
|
||||||
--- @param fn function
|
--- @param fn function
|
||||||
--- @param ctx u.ExecutionContext
|
--- @param ctx u.ExecutionContext
|
||||||
function ExecutionContext:run(fn, ctx)
|
function ExecutionContext.run(fn, ctx)
|
||||||
local oldCtx = CURRENT_CONTEXT
|
local oldCtx = CURRENT_CONTEXT
|
||||||
CURRENT_CONTEXT = ctx
|
CURRENT_CONTEXT = ctx
|
||||||
local result
|
local result
|
||||||
@@ -256,16 +264,18 @@ end
|
|||||||
-- Helpers
|
-- Helpers
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
--- @param value any
|
--- @generic T
|
||||||
|
--- @param value `T`
|
||||||
--- @param name? string
|
--- @param name? string
|
||||||
--- @return u.Signal
|
--- @return u.Signal<T>
|
||||||
function M.create_signal(value, name) return Signal:new(value, name) end
|
function M.create_signal(value, name) return Signal:new(value, name) end
|
||||||
|
|
||||||
--- @param fn function
|
--- @generic T
|
||||||
|
--- @param fn fun(): `T`
|
||||||
--- @param name? string
|
--- @param name? string
|
||||||
--- @return u.Signal
|
--- @return u.Signal
|
||||||
function M.create_memo(fn, name)
|
function M.create_memo(fn, name)
|
||||||
--- @type u.Signal
|
--- @type u.Signal<T> | nil
|
||||||
local result
|
local result
|
||||||
local unsubscribe = M.create_effect(function()
|
local unsubscribe = M.create_effect(function()
|
||||||
local value = fn()
|
local value = fn()
|
||||||
@@ -276,15 +286,15 @@ function M.create_memo(fn, name)
|
|||||||
result = M.create_signal(value, name and ('m.s:' .. name) or nil)
|
result = M.create_signal(value, name and ('m.s:' .. name) or nil)
|
||||||
end
|
end
|
||||||
end, name)
|
end, name)
|
||||||
result:on_dispose(unsubscribe)
|
assert(result):on_dispose(unsubscribe)
|
||||||
return result
|
return assert(result)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param fn function
|
--- @param fn function
|
||||||
--- @param name? string
|
--- @param name? string
|
||||||
function M.create_effect(fn, name)
|
function M.create_effect(fn, name)
|
||||||
local ctx = M.ExecutionContext:new()
|
local ctx = M.ExecutionContext.new()
|
||||||
M.ExecutionContext:run(fn, ctx)
|
M.ExecutionContext.run(fn, ctx)
|
||||||
return ctx:subscribe(function()
|
return ctx:subscribe(function()
|
||||||
if name and M.debug then
|
if name and M.debug then
|
||||||
local deps = vim
|
local deps = vim
|
||||||
|
|||||||
169
lua/u/utils.lua
169
lua/u/utils.lua
@@ -4,14 +4,63 @@ local M = {}
|
|||||||
-- Types
|
-- Types
|
||||||
--
|
--
|
||||||
|
|
||||||
--- @alias QfItem { col: number, filename: string, kind: string, lnum: number, text: string }
|
--- @class u.utils.QfItem
|
||||||
--- @alias KeyMaps table<string, fun(): any | string> }
|
--- @field col number
|
||||||
--- @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 }
|
--- @field filename string
|
||||||
|
--- @field kind string
|
||||||
|
--- @field lnum number
|
||||||
|
--- @field text string
|
||||||
|
|
||||||
|
--- @class u.utils.RawCmdArgs
|
||||||
|
--- @field args string
|
||||||
|
--- @field bang boolean
|
||||||
|
--- @field count number
|
||||||
|
--- @field fargs string[]
|
||||||
|
--- @field line1 number
|
||||||
|
--- @field line2 number
|
||||||
|
--- @field mods string
|
||||||
|
--- @field name string
|
||||||
|
--- @field range 0|1|2
|
||||||
|
--- @field reg string
|
||||||
|
--- @field smods any
|
||||||
|
|
||||||
|
--- @class u.utils.CmdArgs: u.utils.RawCmdArgs
|
||||||
|
--- @field info u.Range|nil
|
||||||
|
|
||||||
|
--- @class u.utils.UcmdArgs
|
||||||
|
--- @field nargs? 0|1|'*'|'?'|'+'
|
||||||
|
--- @field range? boolean|'%'|number
|
||||||
|
--- @field count? boolean|number
|
||||||
|
--- @field addr? string
|
||||||
|
--- @field completion? string
|
||||||
|
--- @field force? boolean
|
||||||
|
--- @field preview? fun(opts: u.utils.UcmdArgs, ns: integer, buf: integer):0|1|2
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Functions
|
||||||
|
--
|
||||||
|
|
||||||
|
--- Debug utility that prints a value and returns it unchanged.
|
||||||
|
--- Useful for debugging in the middle of expressions or function chains.
|
||||||
|
---
|
||||||
--- @generic T
|
--- @generic T
|
||||||
--- @param x `T`
|
--- @param x `T` The value to debug print
|
||||||
--- @param message? string
|
--- @param message? string Optional message to print alongside the value
|
||||||
--- @return T
|
--- @return T The original value, unchanged
|
||||||
|
---
|
||||||
|
--- @usage
|
||||||
|
--- ```lua
|
||||||
|
--- -- Debug a value in the middle of a chain:
|
||||||
|
--- local result = some_function()
|
||||||
|
--- :map(utils.dbg) -- prints the intermediate value
|
||||||
|
--- :filter(predicate)
|
||||||
|
---
|
||||||
|
--- -- Debug with a custom message:
|
||||||
|
--- local config = utils.dbg(get_config(), "Current config:")
|
||||||
|
---
|
||||||
|
--- -- Debug return values:
|
||||||
|
--- return utils.dbg(calculate_result(), "Final result")
|
||||||
|
--- ```
|
||||||
function M.dbg(x, message)
|
function M.dbg(x, message)
|
||||||
local t = {}
|
local t = {}
|
||||||
if message ~= nil then table.insert(t, message) end
|
if message ~= nil then table.insert(t, message) end
|
||||||
@@ -20,21 +69,37 @@ function M.dbg(x, message)
|
|||||||
return x
|
return x
|
||||||
end
|
end
|
||||||
|
|
||||||
--- A utility for creating user commands that also pre-computes useful information
|
--- Creates a user command with enhanced argument processing.
|
||||||
--- and attaches it to the arguments.
|
--- Automatically computes range information and attaches it as `args.info`.
|
||||||
---
|
---
|
||||||
|
--- @param name string The command name (without the leading colon)
|
||||||
|
--- @param cmd string | fun(args: u.utils.CmdArgs): any Command implementation
|
||||||
|
--- @param opts? u.utils.UcmdArgs Command options (nargs, range, etc.)
|
||||||
|
---
|
||||||
|
--- @usage
|
||||||
--- ```lua
|
--- ```lua
|
||||||
--- -- Example:
|
--- -- Create a command that works with visual selections:
|
||||||
--- ucmd('MyCmd', function(args)
|
--- utils.ucmd('MyCmd', function(args)
|
||||||
--- -- print the visually selected text:
|
--- -- Print the visually selected text:
|
||||||
--- vim.print(args.info:text())
|
--- vim.print(args.info:text())
|
||||||
--- -- or get the vtext as an array of lines:
|
--- -- Or get the selection as an array of lines:
|
||||||
--- vim.print(args.info:lines())
|
--- vim.print(args.info:lines())
|
||||||
--- end, { nargs = '*', range = true })
|
--- end, { nargs = '*', range = true })
|
||||||
|
---
|
||||||
|
--- -- Create a command that processes the current line:
|
||||||
|
--- utils.ucmd('ProcessLine', function(args)
|
||||||
|
--- local line_text = args.info:text()
|
||||||
|
--- -- Process the line...
|
||||||
|
--- end, { range = '%' })
|
||||||
|
---
|
||||||
|
--- -- Create a command with arguments:
|
||||||
|
--- utils.ucmd('SearchReplace', function(args)
|
||||||
|
--- local pattern, replacement = args.fargs[1], args.fargs[2]
|
||||||
|
--- local text = args.info:text()
|
||||||
|
--- -- Perform search and replace...
|
||||||
|
--- end, { nargs = 2, range = true })
|
||||||
--- ```
|
--- ```
|
||||||
--- @param name string
|
-- luacheck: ignore
|
||||||
--- @param cmd string | fun(args: CmdArgs): any
|
|
||||||
--- @param opts? { nargs?: 0|1|'*'|'?'|'+'; range?: boolean|'%'|number; count?: boolean|number, addr?: string; completion?: string }
|
|
||||||
function M.ucmd(name, cmd, opts)
|
function M.ucmd(name, cmd, opts)
|
||||||
local Range = require 'u.range'
|
local Range = require 'u.range'
|
||||||
|
|
||||||
@@ -46,9 +111,81 @@ function M.ucmd(name, cmd, opts)
|
|||||||
return cmd(args)
|
return cmd(args)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
vim.api.nvim_create_user_command(name, cmd2, opts or {})
|
vim.api.nvim_create_user_command(name, cmd2, opts or {} --[[@as any]])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Creates command arguments for delegating from one command to another.
|
||||||
|
--- Preserves all relevant context (range, modifiers, bang, etc.) when
|
||||||
|
--- implementing a derived command in terms of a base command.
|
||||||
|
---
|
||||||
|
--- @param current_args vim.api.keyset.create_user_command.command_args|u.utils.RawCmdArgs The arguments from the current command
|
||||||
|
--- @return vim.api.keyset.cmd Arguments suitable for vim.cmd() calls
|
||||||
|
---
|
||||||
|
--- @usage
|
||||||
|
--- ```lua
|
||||||
|
--- -- Implement :MyEdit in terms of :edit, preserving all context:
|
||||||
|
--- utils.ucmd('MyEdit', function(args)
|
||||||
|
--- local delegated_args = utils.create_delegated_cmd_args(args)
|
||||||
|
--- -- Add custom logic here...
|
||||||
|
--- vim.cmd.edit(delegated_args)
|
||||||
|
--- end, { nargs = '*', range = true, bang = true })
|
||||||
|
---
|
||||||
|
--- -- Implement :MySubstitute that delegates to :substitute:
|
||||||
|
--- utils.ucmd('MySubstitute', function(args)
|
||||||
|
--- -- Pre-process arguments
|
||||||
|
--- local pattern = preprocess_pattern(args.fargs[1])
|
||||||
|
---
|
||||||
|
--- local delegated_args = utils.create_delegated_cmd_args(args)
|
||||||
|
--- delegated_args.args = { pattern, args.fargs[2] }
|
||||||
|
---
|
||||||
|
--- vim.cmd.substitute(delegated_args)
|
||||||
|
--- end, { nargs = 2, range = true, bang = true })
|
||||||
|
--- ```
|
||||||
|
function M.create_delegated_cmd_args(current_args)
|
||||||
|
--- @type vim.api.keyset.cmd
|
||||||
|
local args = {
|
||||||
|
range = current_args.range == 1 and { current_args.line1 }
|
||||||
|
or current_args.range == 2 and { current_args.line1, current_args.line2 }
|
||||||
|
or nil,
|
||||||
|
count = (current_args.count ~= -1 and current_args.range == 0) and current_args.count or nil,
|
||||||
|
reg = current_args.reg ~= '' and current_args.reg or nil,
|
||||||
|
bang = current_args.bang or nil,
|
||||||
|
args = #current_args.fargs > 0 and current_args.fargs or nil,
|
||||||
|
mods = current_args.smods,
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Gets the current editor dimensions.
|
||||||
|
--- Useful for positioning floating windows or calculating layout sizes.
|
||||||
|
---
|
||||||
|
--- @return { width: number, height: number } The editor dimensions in columns and lines
|
||||||
|
---
|
||||||
|
--- @usage
|
||||||
|
--- ```lua
|
||||||
|
--- -- Center a floating window:
|
||||||
|
--- local dims = utils.get_editor_dimensions()
|
||||||
|
--- local win_width = 80
|
||||||
|
--- local win_height = 20
|
||||||
|
--- local col = math.floor((dims.width - win_width) / 2)
|
||||||
|
--- local row = math.floor((dims.height - win_height) / 2)
|
||||||
|
---
|
||||||
|
--- vim.api.nvim_open_win(bufnr, true, {
|
||||||
|
--- relative = 'editor',
|
||||||
|
--- width = win_width,
|
||||||
|
--- height = win_height,
|
||||||
|
--- col = col,
|
||||||
|
--- row = row,
|
||||||
|
--- })
|
||||||
|
---
|
||||||
|
--- -- Check if editor is wide enough for side-by-side layout:
|
||||||
|
--- local dims = utils.get_editor_dimensions()
|
||||||
|
--- if dims.width >= 160 then
|
||||||
|
--- -- Use side-by-side layout
|
||||||
|
--- else
|
||||||
|
--- -- Use stacked layout
|
||||||
|
--- end
|
||||||
|
--- ```
|
||||||
function M.get_editor_dimensions() return { width = vim.go.columns, height = vim.go.lines } end
|
function M.get_editor_dimensions() return { width = vim.go.columns, height = vim.go.lines } end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
19
lux.toml
19
lux.toml
@@ -1,19 +0,0 @@
|
|||||||
package = "u.nvim"
|
|
||||||
version = "0.1.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"
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
std = "vim"
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
multiple_statements = "allow"
|
|
||||||
23
shell.nix
Normal file
23
shell.nix
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
pkgs ?
|
||||||
|
import
|
||||||
|
# nixos-unstable (neovim@0.11.4):
|
||||||
|
(fetchTarball {
|
||||||
|
url = "https://github.com/nixos/nixpkgs/archive/0b4defa2584313f3b781240b29d61f6f9f7e0df3.tar.gz";
|
||||||
|
sha256 = "0p3rrd8wwlk0iwgzm7frkw1k98ywrh0avi7fqjjk87i68n3inxrs";
|
||||||
|
})
|
||||||
|
{ },
|
||||||
|
}:
|
||||||
|
pkgs.mkShell {
|
||||||
|
packages = [
|
||||||
|
pkgs.git
|
||||||
|
pkgs.gnumake
|
||||||
|
pkgs.emmylua-check
|
||||||
|
pkgs.lua51Packages.busted
|
||||||
|
pkgs.lua51Packages.luacov
|
||||||
|
pkgs.lua51Packages.luarocks
|
||||||
|
pkgs.neovim
|
||||||
|
pkgs.stylua
|
||||||
|
pkgs.watchexec
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
local Range = require 'u.range'
|
|
||||||
local Pos = require 'u.pos'
|
local Pos = require 'u.pos'
|
||||||
|
local Range = require 'u.range'
|
||||||
local withbuf = loadfile './spec/withbuf.lua'()
|
local withbuf = loadfile './spec/withbuf.lua'()
|
||||||
|
|
||||||
describe('Range', function()
|
describe('Range', function()
|
||||||
@@ -238,6 +238,128 @@ describe('Range', function()
|
|||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it('from_tsquery_caps', function()
|
||||||
|
withbuf({
|
||||||
|
'-- a comment',
|
||||||
|
'',
|
||||||
|
'function foo(bar) end',
|
||||||
|
'',
|
||||||
|
'-- a middle comment',
|
||||||
|
'',
|
||||||
|
'function bar(baz) end',
|
||||||
|
'',
|
||||||
|
'-- another comment',
|
||||||
|
}, function()
|
||||||
|
vim.cmd.setfiletype 'lua'
|
||||||
|
|
||||||
|
--- @param contains_cursor? boolean
|
||||||
|
local function get_caps(contains_cursor)
|
||||||
|
if contains_cursor == nil then contains_cursor = true end
|
||||||
|
return (Range.from_tsquery_caps(
|
||||||
|
0,
|
||||||
|
'(function_declaration) @f',
|
||||||
|
{ contains_cursor = contains_cursor }
|
||||||
|
)).f or {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local caps = get_caps(false)
|
||||||
|
assert.are.same(#caps, 2)
|
||||||
|
assert.are.same(vim.iter(caps):map(function(c) return c:text() end):totable(), {
|
||||||
|
'function foo(bar) end',
|
||||||
|
'function bar(baz) end',
|
||||||
|
})
|
||||||
|
|
||||||
|
Pos.new(0, 1, 1):save_to_pos '.'
|
||||||
|
caps = get_caps()
|
||||||
|
assert.are.same(#caps, 0)
|
||||||
|
|
||||||
|
Pos.new(0, 3, 18):save_to_pos '.'
|
||||||
|
caps = get_caps()
|
||||||
|
assert.are.same(#caps, 1)
|
||||||
|
assert.are.same(caps[1]:text(), 'function foo(bar) end')
|
||||||
|
|
||||||
|
Pos.new(0, 5, 1):save_to_pos '.'
|
||||||
|
caps = get_caps()
|
||||||
|
assert.are.same(#caps, 0)
|
||||||
|
|
||||||
|
Pos.new(0, 7, 1):save_to_pos '.'
|
||||||
|
caps = get_caps()
|
||||||
|
assert.are.same(#caps, 1)
|
||||||
|
assert.are.same(caps[1]:text(), 'function bar(baz) end')
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('from_tsquery_caps with string array filter', function()
|
||||||
|
withbuf({
|
||||||
|
'{',
|
||||||
|
' sample_key1 = "sample-value1",',
|
||||||
|
' sample_key2 = "sample-value2",',
|
||||||
|
'}',
|
||||||
|
}, function()
|
||||||
|
vim.cmd.setfiletype 'lua'
|
||||||
|
|
||||||
|
-- Place cursor in "sample-value1"
|
||||||
|
Pos.new(0, 2, 25):save_to_pos '.'
|
||||||
|
|
||||||
|
-- Query that captures both keys and values in pairs
|
||||||
|
local query = [[
|
||||||
|
(field
|
||||||
|
name: _ @key
|
||||||
|
value: _ @value)
|
||||||
|
]]
|
||||||
|
|
||||||
|
local ranges = Range.from_line(0, 2):tsquery(query)
|
||||||
|
|
||||||
|
-- Should have both @key and @value captures for the first pair only
|
||||||
|
-- (since cursor is in sample-value1)
|
||||||
|
assert(ranges, 'Range should not be nil')
|
||||||
|
assert(ranges.key, 'Range.key should not be nil')
|
||||||
|
assert(ranges.value, 'Range.value should not be nil')
|
||||||
|
|
||||||
|
-- Should have exactly one key and one value
|
||||||
|
assert.are.same(#ranges.key, 1)
|
||||||
|
assert.are.same(#ranges.value, 1)
|
||||||
|
|
||||||
|
-- Check that we got sample-key1 and sample-value1
|
||||||
|
assert.are.same(ranges.key[1]:text(), 'sample_key1')
|
||||||
|
assert.are.same(ranges.value[1]:text(), '"sample-value1"')
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Make sure this works when the match is on the last line:
|
||||||
|
withbuf({
|
||||||
|
'{sample_key1= "sample-value1",',
|
||||||
|
'sample_key2= "sample-value2"}',
|
||||||
|
}, function()
|
||||||
|
vim.cmd.setfiletype 'lua'
|
||||||
|
|
||||||
|
-- Place cursor in "sample-value1"
|
||||||
|
Pos.new(0, 2, 25):save_to_pos '.'
|
||||||
|
|
||||||
|
-- Query that captures both keys and values in pairs
|
||||||
|
local query = [[
|
||||||
|
(field
|
||||||
|
name: _ @key
|
||||||
|
value: _ @value)
|
||||||
|
]]
|
||||||
|
|
||||||
|
local ranges = Range.from_line(0, 2):tsquery(query)
|
||||||
|
|
||||||
|
-- Should have both @key and @value captures for the first pair only
|
||||||
|
-- (since cursor is in sample-value1)
|
||||||
|
assert(ranges, 'Range should not be nil')
|
||||||
|
assert(ranges.key, 'Range.key should not be nil')
|
||||||
|
assert(ranges.value, 'Range.value should not be nil')
|
||||||
|
|
||||||
|
-- Should have exactly one key and one value
|
||||||
|
assert.are.same(#ranges.key, 1)
|
||||||
|
assert.are.same(#ranges.value, 1)
|
||||||
|
|
||||||
|
-- Check that we got sample-key2 and sample-value2
|
||||||
|
assert.are.same(ranges.key[1]:text(), 'sample_key2')
|
||||||
|
assert.are.same(ranges.value[1]:text(), '"sample-value2"')
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
it('should get nearest block', function()
|
it('should get nearest block', function()
|
||||||
withbuf({
|
withbuf({
|
||||||
'this is a {',
|
'this is a {',
|
||||||
@@ -318,18 +440,89 @@ describe('Range', function()
|
|||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('from_cmd_args', function()
|
it('from_cmd_args: range=0', function()
|
||||||
local args = { range = 1 }
|
local args = { range = 0 }
|
||||||
|
withbuf(
|
||||||
|
{ 'line one', 'and line two' },
|
||||||
|
function() assert.are.same(Range.from_cmd_args(args), nil) end
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('from_cmd_args: range=1', function()
|
||||||
|
local args = { range = 1, line1 = 1 }
|
||||||
withbuf({ 'line one', 'and line two' }, function()
|
withbuf({ 'line one', 'and line two' }, function()
|
||||||
local a = Pos.new(nil, 1, 1)
|
local a = Pos.new(nil, 1, 1)
|
||||||
local b = Pos.new(nil, 2, 2)
|
local b = Pos.new(nil, 1, Pos.MAX_COL)
|
||||||
|
|
||||||
|
local range = Range.from_cmd_args(args) --[[@as u.Range]]
|
||||||
|
assert.are.same(range.start, a)
|
||||||
|
assert.are.same(range.stop, b)
|
||||||
|
assert.are.same(range.mode, 'V')
|
||||||
|
assert.are.same(range:text(), 'line one')
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('from_cmd_args: range=2: no-visual', function()
|
||||||
|
local args = { range = 2, line1 = 1, line2 = 2 }
|
||||||
|
withbuf({ 'line one', 'and line two' }, function()
|
||||||
|
local range = Range.from_cmd_args(args) --[[@as u.Range]]
|
||||||
|
assert.are.same(range.start, Pos.new(nil, 1, 1))
|
||||||
|
assert.are.same(range.stop, Pos.new(nil, 2, Pos.MAX_COL))
|
||||||
|
assert.are.same(range.mode, 'V')
|
||||||
|
assert.are.same(range:text(), 'line one\nand line two')
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('from_cmd_args: range=2: visual: linewise', function()
|
||||||
|
local args = { range = 2, line1 = 1, line2 = 2 }
|
||||||
|
withbuf({ 'line one', 'and line two' }, function()
|
||||||
|
local a = Pos.new(nil, 1, 1)
|
||||||
|
local b = Pos.new(nil, 2, Pos.MAX_COL)
|
||||||
a:save_to_pos "'<"
|
a:save_to_pos "'<"
|
||||||
b:save_to_pos "'>"
|
b:save_to_pos "'>"
|
||||||
|
local range = Range.from_cmd_args(args) --[[@as u.Range]]
|
||||||
|
assert.are.same(range.start, a)
|
||||||
|
assert.are.same(range.stop, b)
|
||||||
|
assert.are.same(range.mode, 'V')
|
||||||
|
assert.are.same(range:text(), 'line one\nand line two')
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
local range = Range.from_cmd_args(args)
|
it('from_cmd_args: range=2: visual: charwise', function()
|
||||||
|
local args = { range = 2, line1 = 1, line2 = 1 }
|
||||||
|
withbuf({ 'line one', 'and line two' }, function()
|
||||||
|
-- Simulate a visual selection:
|
||||||
|
local a = Pos.new(nil, 1, 1)
|
||||||
|
local b = Pos.new(nil, 1, 4)
|
||||||
|
a:save_to_pos "'<"
|
||||||
|
b:save_to_pos "'>"
|
||||||
|
Range.new(a, b, 'v'):set_visual_selection()
|
||||||
|
assert.are.same(vim.fn.mode(), 'v')
|
||||||
|
|
||||||
|
-- In this simulated setup, we need to force visualmode() to return
|
||||||
|
-- 'v' and histget() to return [['<,'>]]:
|
||||||
|
|
||||||
|
-- visualmode()
|
||||||
|
local orig_visualmode = vim.fn.visualmode
|
||||||
|
--- @diagnostic disable-next-line: duplicate-set-field
|
||||||
|
function vim.fn.visualmode() return 'v' end
|
||||||
|
assert.are.same(vim.fn.visualmode(), 'v')
|
||||||
|
|
||||||
|
-- histget()
|
||||||
|
local orig_histget = vim.fn.histget
|
||||||
|
--- @diagnostic disable-next-line: duplicate-set-field
|
||||||
|
function vim.fn.histget(x, y) return [['<,'>]] end
|
||||||
|
|
||||||
|
-- Now run the test:
|
||||||
|
local range = Range.from_cmd_args(args) --[[@as u.Range]]
|
||||||
assert.are.same(range.start, a)
|
assert.are.same(range.start, a)
|
||||||
assert.are.same(range.stop, b)
|
assert.are.same(range.stop, b)
|
||||||
assert.are.same(range.mode, 'v')
|
assert.are.same(range.mode, 'v')
|
||||||
|
assert.are.same(range:text(), 'line')
|
||||||
|
|
||||||
|
-- Reset visualmode() and histget():
|
||||||
|
vim.fn.visualmode = orig_visualmode
|
||||||
|
vim.fn.histget = orig_histget
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -422,6 +615,52 @@ describe('Range', function()
|
|||||||
end)
|
end)
|
||||||
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()
|
it('shrink', function()
|
||||||
withbuf({ 'line one', 'and line two' }, function()
|
withbuf({ 'line one', 'and line two' }, function()
|
||||||
local range = Range.new(Pos.new(nil, 2, 3), Pos.new(nil, 3, 5), 'v')
|
local range = Range.new(Pos.new(nil, 2, 3), Pos.new(nil, 3, 5), 'v')
|
||||||
@@ -438,7 +677,10 @@ describe('Range', function()
|
|||||||
assert.are.same(shrunk.start, Pos.new(nil, 2, 4))
|
assert.are.same(shrunk.start, Pos.new(nil, 2, 4))
|
||||||
assert.are.same(shrunk.stop, Pos.new(nil, 3, 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)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -476,10 +718,16 @@ describe('Range', function()
|
|||||||
local r = Range.new(Pos.new(b, 1, 5), Pos.new(b, 1, 9), 'v')
|
local r = Range.new(Pos.new(b, 1, 5), Pos.new(b, 1, 9), 'v')
|
||||||
|
|
||||||
r:replace 'bleh1'
|
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'
|
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)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -493,11 +741,17 @@ describe('Range', function()
|
|||||||
assert.are.same({ 'jumps', 'over' }, r:lines())
|
assert.are.same({ 'jumps', 'over' }, r:lines())
|
||||||
|
|
||||||
r:replace 'bleh1'
|
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())
|
assert.are.same({ 'bleh1' }, r:lines())
|
||||||
|
|
||||||
r:replace 'blehGoo2'
|
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)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -556,4 +810,86 @@ describe('Range', function()
|
|||||||
}, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
}, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it('can save to extmark', function()
|
||||||
|
withbuf({
|
||||||
|
'The quick brown',
|
||||||
|
'fox',
|
||||||
|
'jumps',
|
||||||
|
'over',
|
||||||
|
'the lazy dog',
|
||||||
|
}, function()
|
||||||
|
-- Construct a range over 'fox jumps'
|
||||||
|
local r = Range.new(Pos.new(nil, 2, 1), Pos.new(nil, 3, 5), 'v')
|
||||||
|
local extrange = r:save_to_extmark()
|
||||||
|
assert.are.same({ 'fox', 'jumps' }, extrange:range():lines())
|
||||||
|
-- change 'jumps' to 'leaps':
|
||||||
|
vim.api.nvim_buf_set_text(extrange.bufnr, 2, 0, 2, 4, { 'leap' })
|
||||||
|
assert.are.same({
|
||||||
|
'The quick brown',
|
||||||
|
'fox',
|
||||||
|
'leaps',
|
||||||
|
'over',
|
||||||
|
'the lazy dog',
|
||||||
|
}, vim.api.nvim_buf_get_lines(extrange.bufnr, 0, -1, false))
|
||||||
|
assert.are.same({ 'fox', 'leaps' }, extrange:range():lines())
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('can save linewise extmark', function()
|
||||||
|
withbuf({
|
||||||
|
'The quick brown',
|
||||||
|
'fox',
|
||||||
|
'jumps',
|
||||||
|
'over',
|
||||||
|
'the lazy dog',
|
||||||
|
}, function()
|
||||||
|
-- Construct a range over 'fox jumps'
|
||||||
|
local r = Range.new(Pos.new(nil, 2, 1), Pos.new(nil, 3, Pos.MAX_COL), 'V')
|
||||||
|
local extrange = r:save_to_extmark()
|
||||||
|
assert.are.same({ 'fox', 'jumps' }, extrange:range():lines())
|
||||||
|
|
||||||
|
local extmark = vim.api.nvim_buf_get_extmark_by_id(
|
||||||
|
extrange.bufnr,
|
||||||
|
vim.api.nvim_create_namespace 'u.range',
|
||||||
|
extrange.id,
|
||||||
|
{ details = true }
|
||||||
|
)
|
||||||
|
local row0, col0, details = unpack(extmark)
|
||||||
|
assert.are.same({ 1, 0 }, { row0, col0 })
|
||||||
|
assert.are.same({ 3, 0 }, { details.end_row, details.end_col })
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('discerns range bounds from extmarks beyond the end of the buffer', function()
|
||||||
|
local Buffer = require 'u.buffer'
|
||||||
|
|
||||||
|
vim.cmd.vnew()
|
||||||
|
local left = Buffer.current()
|
||||||
|
left:set_tmp_options()
|
||||||
|
vim.cmd.vnew()
|
||||||
|
local right = Buffer.current()
|
||||||
|
right:set_tmp_options()
|
||||||
|
|
||||||
|
left:all():replace {
|
||||||
|
'one',
|
||||||
|
'two',
|
||||||
|
'three',
|
||||||
|
}
|
||||||
|
local left_all_ext = left:all():save_to_extmark()
|
||||||
|
|
||||||
|
right:all():replace {
|
||||||
|
'foo',
|
||||||
|
'bar',
|
||||||
|
}
|
||||||
|
|
||||||
|
vim.api.nvim_set_current_buf(right.bufnr)
|
||||||
|
vim.cmd [[normal! ggyG]]
|
||||||
|
vim.api.nvim_set_current_buf(left.bufnr)
|
||||||
|
vim.cmd [[normal! ggVGp]]
|
||||||
|
|
||||||
|
assert.are.same({ 'foo', 'bar' }, left_all_ext:range():lines())
|
||||||
|
vim.api.nvim_buf_delete(left.bufnr, { force = true })
|
||||||
|
vim.api.nvim_buf_delete(right.bufnr, { force = true })
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -83,13 +83,13 @@ describe('Renderer', function()
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
--
|
--
|
||||||
-- get_pos_infos
|
-- get_tags_at
|
||||||
--
|
--
|
||||||
|
|
||||||
it('should return no extmarks for an empty buffer', function()
|
it('should return no extmarks for an empty buffer', function()
|
||||||
withbuf({}, function()
|
withbuf({}, function()
|
||||||
local r = R.Renderer.new(0)
|
local r = R.Renderer.new(0)
|
||||||
local pos_infos = r:get_pos_infos { 0, 0 }
|
local pos_infos = r:get_tags_at { 0, 0 }
|
||||||
assert.are.same(pos_infos, {})
|
assert.are.same(pos_infos, {})
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
@@ -102,12 +102,28 @@ describe('Renderer', function()
|
|||||||
R.h('text', { hl = 'HighlightGroup2' }, ' World'),
|
R.h('text', { hl = 'HighlightGroup2' }, ' World'),
|
||||||
}
|
}
|
||||||
|
|
||||||
local pos_infos = r:get_pos_infos { 0, 2 }
|
local pos_infos = r:get_tags_at { 0, 2 }
|
||||||
|
|
||||||
assert.are.same(#pos_infos, 1)
|
assert.are.same(#pos_infos, 1)
|
||||||
assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup1')
|
assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup1')
|
||||||
assert.are.same(pos_infos[1].extmark.start, { 0, 0 })
|
assert.are.same(pos_infos[1].extmark.start, { 0, 0 })
|
||||||
assert.are.same(pos_infos[1].extmark.stop, { 0, 5 })
|
assert.are.same(pos_infos[1].extmark.stop, { 0, 5 })
|
||||||
|
pos_infos = r:get_tags_at { 0, 4 }
|
||||||
|
assert.are.same(#pos_infos, 1)
|
||||||
|
assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup1')
|
||||||
|
assert.are.same(pos_infos[1].extmark.start, { 0, 0 })
|
||||||
|
assert.are.same(pos_infos[1].extmark.stop, { 0, 5 })
|
||||||
|
|
||||||
|
pos_infos = r:get_tags_at { 0, 5 }
|
||||||
|
assert.are.same(#pos_infos, 1)
|
||||||
|
assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup2')
|
||||||
|
assert.are.same(pos_infos[1].extmark.start, { 0, 5 })
|
||||||
|
assert.are.same(pos_infos[1].extmark.stop, { 0, 11 })
|
||||||
|
|
||||||
|
-- In insert mode, bounds are eagerly included:
|
||||||
|
pos_infos = r:get_tags_at({ 0, 5 }, 'i')
|
||||||
|
assert.are.same(#pos_infos, 2)
|
||||||
|
assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup1')
|
||||||
|
assert.are.same(pos_infos[2].tag.attributes.hl, 'HighlightGroup2')
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -117,15 +133,260 @@ describe('Renderer', function()
|
|||||||
r:render {
|
r:render {
|
||||||
R.h('text', { hl = 'HighlightGroup1' }, {
|
R.h('text', { hl = 'HighlightGroup1' }, {
|
||||||
'Hello',
|
'Hello',
|
||||||
R.h('text', { hl = 'HighlightGroup2', extmark = { hl_group = 'HighlightGroup2' } }, ' World'),
|
R.h(
|
||||||
|
'text',
|
||||||
|
{ hl = 'HighlightGroup2', extmark = { hl_group = 'HighlightGroup2' } },
|
||||||
|
' World'
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
local pos_infos = r:get_pos_infos { 0, 5 }
|
local pos_infos = r:get_tags_at { 0, 5 }
|
||||||
|
|
||||||
assert.are.same(#pos_infos, 2)
|
assert.are.same(#pos_infos, 2)
|
||||||
assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup2')
|
assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup2')
|
||||||
assert.are.same(pos_infos[2].tag.attributes.hl, 'HighlightGroup1')
|
assert.are.same(pos_infos[2].tag.attributes.hl, 'HighlightGroup1')
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it('repeated patch_lines calls should not change the buffer content', function()
|
||||||
|
local lines = {
|
||||||
|
[[{ {]],
|
||||||
|
[[ bounds = {]],
|
||||||
|
[[ start1 = { 1, 1 },]],
|
||||||
|
[[ stop1 = { 4, 1 }]],
|
||||||
|
[[ },]],
|
||||||
|
[[ end_right_gravity = true,]],
|
||||||
|
[[ id = 1,]],
|
||||||
|
[[ ns_id = 623,]],
|
||||||
|
[[ ns_name = "my.renderer:91",]],
|
||||||
|
[[ right_gravity = false]],
|
||||||
|
[[ } }]],
|
||||||
|
[[]],
|
||||||
|
}
|
||||||
|
withbuf(lines, function()
|
||||||
|
local Buffer = require 'u.buffer'
|
||||||
|
R.Renderer.patch_lines(0, nil, lines)
|
||||||
|
assert.are.same(Buffer.current():all():lines(), lines)
|
||||||
|
|
||||||
|
R.Renderer.patch_lines(0, lines, lines)
|
||||||
|
assert.are.same(Buffer.current():all():lines(), lines)
|
||||||
|
|
||||||
|
R.Renderer.patch_lines(0, lines, lines)
|
||||||
|
assert.are.same(Buffer.current():all():lines(), lines)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('should fire text-changed events', function()
|
||||||
|
withbuf({}, function()
|
||||||
|
local Buffer = require 'u.buffer'
|
||||||
|
local buf = Buffer.current()
|
||||||
|
local r = R.Renderer.new(0)
|
||||||
|
local captured_changed_text = ''
|
||||||
|
r:render {
|
||||||
|
R.h('text', {
|
||||||
|
on_change = function(txt) captured_changed_text = txt end,
|
||||||
|
}, {
|
||||||
|
'one\n',
|
||||||
|
'two\n',
|
||||||
|
'three\n',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
vim.fn.setreg('"', 'bleh')
|
||||||
|
vim.cmd [[normal! ggVGp]]
|
||||||
|
-- For some reason, the autocmd does not fire in the busted environment.
|
||||||
|
-- We'll call the handler ourselves:
|
||||||
|
r:_on_text_changed()
|
||||||
|
|
||||||
|
assert.are.same(buf:all():text(), 'bleh')
|
||||||
|
assert.are.same(captured_changed_text, 'bleh')
|
||||||
|
|
||||||
|
vim.fn.setreg('"', '')
|
||||||
|
vim.cmd [[normal! ggdG]]
|
||||||
|
-- We'll call the handler ourselves:
|
||||||
|
r:_on_text_changed()
|
||||||
|
|
||||||
|
assert.are.same(buf:all():text(), '')
|
||||||
|
assert.are.same(captured_changed_text, '')
|
||||||
|
end)
|
||||||
|
|
||||||
|
withbuf({}, function()
|
||||||
|
local Buffer = require 'u.buffer'
|
||||||
|
local buf = Buffer.current()
|
||||||
|
local r = R.Renderer.new(0)
|
||||||
|
--- @type string?
|
||||||
|
local captured_changed_text = nil
|
||||||
|
r:render {
|
||||||
|
'prefix:',
|
||||||
|
R.h('text', {
|
||||||
|
on_change = function(txt) captured_changed_text = txt end,
|
||||||
|
}, {
|
||||||
|
'one',
|
||||||
|
}),
|
||||||
|
'suffix',
|
||||||
|
}
|
||||||
|
|
||||||
|
vim.fn.setreg('"', 'bleh')
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 1, 9 })
|
||||||
|
vim.cmd [[normal! vhhd]]
|
||||||
|
-- For some reason, the autocmd does not fire in the busted environment.
|
||||||
|
-- We'll call the handler ourselves:
|
||||||
|
r:_on_text_changed()
|
||||||
|
|
||||||
|
assert.are.same(buf:all():text(), 'prefix:suffix')
|
||||||
|
assert.are.same(captured_changed_text, '')
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('should find tags by position', function()
|
||||||
|
withbuf({}, function()
|
||||||
|
local r = R.Renderer.new(0)
|
||||||
|
r:render {
|
||||||
|
'pre',
|
||||||
|
R.h('text', {
|
||||||
|
id = 'outer',
|
||||||
|
}, {
|
||||||
|
'inner-pre',
|
||||||
|
R.h('text', {
|
||||||
|
id = 'inner',
|
||||||
|
}, {
|
||||||
|
'inner-text',
|
||||||
|
}),
|
||||||
|
'inner-post',
|
||||||
|
}),
|
||||||
|
'post',
|
||||||
|
}
|
||||||
|
|
||||||
|
local tags = r:get_tags_at { 0, 11 }
|
||||||
|
assert.are.same(#tags, 1)
|
||||||
|
assert.are.same(tags[1].tag.attributes.id, 'outer')
|
||||||
|
|
||||||
|
tags = r:get_tags_at { 0, 12 }
|
||||||
|
assert.are.same(#tags, 2)
|
||||||
|
assert.are.same(tags[1].tag.attributes.id, 'inner')
|
||||||
|
assert.are.same(tags[2].tag.attributes.id, 'outer')
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('should find tags by id', function()
|
||||||
|
withbuf({}, function()
|
||||||
|
local r = R.Renderer.new(0)
|
||||||
|
r:render {
|
||||||
|
R.h('text', {
|
||||||
|
id = 'outer',
|
||||||
|
}, {
|
||||||
|
'inner-pre',
|
||||||
|
R.h('text', {
|
||||||
|
id = 'inner',
|
||||||
|
}, {
|
||||||
|
'inner-text',
|
||||||
|
}),
|
||||||
|
'inner-post',
|
||||||
|
}),
|
||||||
|
'post',
|
||||||
|
}
|
||||||
|
|
||||||
|
local bounds = r:get_tag_bounds 'outer'
|
||||||
|
assert.are.same(bounds, { start = { 0, 0 }, stop = { 0, 29 } })
|
||||||
|
|
||||||
|
bounds = r:get_tag_bounds 'inner'
|
||||||
|
assert.are.same(bounds, { start = { 0, 9 }, stop = { 0, 19 } })
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('should mount and rerender components', function()
|
||||||
|
withbuf({}, function()
|
||||||
|
--- @type any
|
||||||
|
local leaked_state = { app = {}, c1 = {}, c2 = {} }
|
||||||
|
|
||||||
|
--- @param ctx u.renderer.FnComponentContext<any, { phase: string, count: integer }>
|
||||||
|
local function Counter(ctx)
|
||||||
|
if ctx.phase == 'mount' then ctx.state = { phase = ctx.phase, count = 1 } end
|
||||||
|
local state = assert(ctx.state)
|
||||||
|
state.phase = ctx.phase
|
||||||
|
leaked_state[ctx.props.id].ctx = ctx
|
||||||
|
|
||||||
|
return {
|
||||||
|
{ 'Value: ', R.h.Number({}, tostring(state.count)) },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param ctx u.renderer.FnComponentContext<any, {
|
||||||
|
--- toggle1: boolean,
|
||||||
|
--- show2: boolean
|
||||||
|
--- }>
|
||||||
|
function App(ctx)
|
||||||
|
if ctx.phase == 'mount' then ctx.state = { toggle1 = false, show2 = true } end
|
||||||
|
local state = assert(ctx.state)
|
||||||
|
leaked_state.app.ctx = ctx
|
||||||
|
|
||||||
|
return {
|
||||||
|
state.toggle1 and 'Toggle1' or R.h(Counter, { id = 'c1' }, {}),
|
||||||
|
'\n',
|
||||||
|
|
||||||
|
state.show2 and {
|
||||||
|
'\n',
|
||||||
|
R.h(Counter, { id = 'c2' }, {}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local renderer = R.Renderer.new()
|
||||||
|
renderer:mount(R.h(App, {}, {}))
|
||||||
|
|
||||||
|
local Buffer = require 'u.buffer'
|
||||||
|
local buf = Buffer.current()
|
||||||
|
assert.are.same(buf:all():lines(), {
|
||||||
|
'Value: 1',
|
||||||
|
'',
|
||||||
|
'Value: 1',
|
||||||
|
})
|
||||||
|
assert.are.same(leaked_state.c1.ctx.state.phase, 'mount')
|
||||||
|
assert.are.same(leaked_state.c2.ctx.state.phase, 'mount')
|
||||||
|
leaked_state.app.ctx:update_immediate { toggle1 = true, show2 = true }
|
||||||
|
assert.are.same(buf:all():lines(), {
|
||||||
|
'Toggle1',
|
||||||
|
'',
|
||||||
|
'Value: 1',
|
||||||
|
})
|
||||||
|
assert.are.same(leaked_state.c1.ctx.state.phase, 'unmount')
|
||||||
|
assert.are.same(leaked_state.c2.ctx.state.phase, 'update')
|
||||||
|
|
||||||
|
leaked_state.app.ctx:update_immediate { toggle1 = true, show2 = false }
|
||||||
|
assert.are.same(buf:all():lines(), {
|
||||||
|
'Toggle1',
|
||||||
|
'',
|
||||||
|
})
|
||||||
|
assert.are.same(leaked_state.c1.ctx.state.phase, 'unmount')
|
||||||
|
assert.are.same(leaked_state.c2.ctx.state.phase, 'unmount')
|
||||||
|
|
||||||
|
leaked_state.app.ctx:update_immediate { toggle1 = false, show2 = true }
|
||||||
|
assert.are.same(buf:all():lines(), {
|
||||||
|
'Value: 1',
|
||||||
|
'',
|
||||||
|
'Value: 1',
|
||||||
|
})
|
||||||
|
assert.are.same(leaked_state.c1.ctx.state.phase, 'mount')
|
||||||
|
assert.are.same(leaked_state.c2.ctx.state.phase, 'mount')
|
||||||
|
|
||||||
|
leaked_state.c1.ctx:update_immediate { count = 2 }
|
||||||
|
assert.are.same(buf:all():lines(), {
|
||||||
|
'Value: 2',
|
||||||
|
'',
|
||||||
|
'Value: 1',
|
||||||
|
})
|
||||||
|
assert.are.same(leaked_state.c1.ctx.state.phase, 'update')
|
||||||
|
assert.are.same(leaked_state.c2.ctx.state.phase, 'update')
|
||||||
|
|
||||||
|
leaked_state.c2.ctx:update_immediate { count = 3 }
|
||||||
|
assert.are.same(buf:all():lines(), {
|
||||||
|
'Value: 2',
|
||||||
|
'',
|
||||||
|
'Value: 3',
|
||||||
|
})
|
||||||
|
assert.are.same(leaked_state.c1.ctx.state.phase, 'update')
|
||||||
|
assert.are.same(leaked_state.c2.ctx.state.phase, 'update')
|
||||||
|
end)
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -58,66 +58,66 @@ describe('Signal', function()
|
|||||||
|
|
||||||
describe('Signal:map', function()
|
describe('Signal:map', function()
|
||||||
it('should transform the signal value', function()
|
it('should transform the signal value', function()
|
||||||
local signal = Signal:new(5)
|
local test_signal = Signal:new(5)
|
||||||
local mapped_signal = signal:map(function(value) return value * 2 end)
|
local mapped_signal = test_signal:map(function(value) return value * 2 end)
|
||||||
|
|
||||||
assert.is.equal(mapped_signal:get(), 10) -- Initial transformation
|
assert.is.equal(mapped_signal:get(), 10) -- Initial transformation
|
||||||
signal:set(10)
|
test_signal:set(10)
|
||||||
assert.is.equal(mapped_signal:get(), 20) -- Updated transformation
|
assert.is.equal(mapped_signal:get(), 20) -- Updated transformation
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('should handle empty transformations', function()
|
it('should handle empty transformations', function()
|
||||||
local signal = Signal:new(nil)
|
local test_signal = Signal:new(nil)
|
||||||
local mapped_signal = signal:map(function(value) return value or 'default' end)
|
local mapped_signal = test_signal:map(function(value) return value or 'default' end)
|
||||||
|
|
||||||
assert.is.equal(mapped_signal:get(), 'default') -- Return default
|
assert.is.equal(mapped_signal:get(), 'default') -- Return default
|
||||||
signal:set 'new value'
|
test_signal:set 'new value'
|
||||||
assert.is.equal(mapped_signal:get(), 'new value') -- Return new value
|
assert.is.equal(mapped_signal:get(), 'new value') -- Return new value
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
describe('Signal:filter', function()
|
describe('Signal:filter', function()
|
||||||
it('should only emit values that pass the filter', function()
|
it('should only emit values that pass the filter', function()
|
||||||
local signal = Signal:new(5)
|
local test_signal = Signal:new(5)
|
||||||
local filtered_signal = signal:filter(function(value) return value > 10 end)
|
local filtered_signal = test_signal:filter(function(value) return value > 10 end)
|
||||||
|
|
||||||
assert.is.equal(filtered_signal:get(), nil) -- Initial value should not pass
|
assert.is.equal(filtered_signal:get(), nil) -- Initial value should not pass
|
||||||
signal:set(15)
|
test_signal:set(15)
|
||||||
assert.is.equal(filtered_signal:get(), 15) -- Now filtered
|
assert.is.equal(filtered_signal:get(), 15) -- Now filtered
|
||||||
signal:set(8)
|
test_signal:set(8)
|
||||||
assert.is.equal(filtered_signal:get(), 15) -- Does not pass the filter
|
assert.is.equal(filtered_signal:get(), 15) -- Does not pass the filter
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('should handle empty initial values', function()
|
it('should handle empty initial values', function()
|
||||||
local signal = Signal:new(nil)
|
local test_signal = Signal:new(nil)
|
||||||
local filtered_signal = signal:filter(function(value) return value ~= nil end)
|
local filtered_signal = test_signal:filter(function(value) return value ~= nil end)
|
||||||
|
|
||||||
assert.is.equal(filtered_signal:get(), nil) -- Should be nil
|
assert.is.equal(filtered_signal:get(), nil) -- Should be nil
|
||||||
signal:set(10)
|
test_signal:set(10)
|
||||||
assert.is.equal(filtered_signal:get(), 10) -- Should pass now
|
assert.is.equal(filtered_signal:get(), 10) -- Should pass now
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
describe('create_memo', function()
|
describe('create_memo', function()
|
||||||
it('should compute a derived value and update when dependencies change', function()
|
it('should compute a derived value and update when dependencies change', function()
|
||||||
local signal = Signal:new(2)
|
local test_signal = Signal:new(2)
|
||||||
local memoized_signal = tracker.create_memo(function() return signal:get() * 2 end)
|
local memoized_signal = tracker.create_memo(function() return test_signal:get() * 2 end)
|
||||||
|
|
||||||
assert.is.equal(memoized_signal:get(), 4) -- Initially compute 2 * 2
|
assert.is.equal(memoized_signal:get(), 4) -- Initially compute 2 * 2
|
||||||
|
|
||||||
signal:set(3)
|
test_signal:set(3)
|
||||||
assert.is.equal(memoized_signal:get(), 6) -- Update to 3 * 2 = 6
|
assert.is.equal(memoized_signal:get(), 6) -- Update to 3 * 2 = 6
|
||||||
|
|
||||||
signal:set(5)
|
test_signal:set(5)
|
||||||
assert.is.equal(memoized_signal:get(), 10) -- Update to 5 * 2 = 10
|
assert.is.equal(memoized_signal:get(), 10) -- Update to 5 * 2 = 10
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('should not recompute if the dependencies do not change', function()
|
it('should not recompute if the dependencies do not change', function()
|
||||||
local call_count = 0
|
local call_count = 0
|
||||||
local signal = Signal:new(10)
|
local test_signal = Signal:new(10)
|
||||||
local memoized_signal = tracker.create_memo(function()
|
local memoized_signal = tracker.create_memo(function()
|
||||||
call_count = call_count + 1
|
call_count = call_count + 1
|
||||||
return signal:get() + 1
|
return test_signal:get() + 1
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert.is.equal(memoized_signal:get(), 11) -- Compute first value
|
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
|
memoized_signal:get() -- Call again, should use memoized value
|
||||||
assert.is.equal(call_count, 1) -- Still should only be one call
|
assert.is.equal(call_count, 1) -- Still should only be one call
|
||||||
|
|
||||||
signal:set(10) -- Set the same value
|
test_signal:set(10) -- Set the same value
|
||||||
assert.is.equal(memoized_signal:get(), 11)
|
assert.is.equal(memoized_signal:get(), 11)
|
||||||
assert.is.equal(call_count, 2)
|
assert.is.equal(call_count, 2)
|
||||||
|
|
||||||
signal:set(20)
|
test_signal:set(20)
|
||||||
assert.is.equal(memoized_signal:get(), 21)
|
assert.is.equal(memoized_signal:get(), 21)
|
||||||
assert.is.equal(call_count, 3)
|
assert.is.equal(call_count, 3)
|
||||||
end)
|
end)
|
||||||
@@ -138,31 +138,31 @@ describe('Signal', function()
|
|||||||
|
|
||||||
describe('create_effect', function()
|
describe('create_effect', function()
|
||||||
it('should track changes and execute callback', function()
|
it('should track changes and execute callback', function()
|
||||||
local signal = Signal:new(5)
|
local test_signal = Signal:new(5)
|
||||||
local call_count = 0
|
local call_count = 0
|
||||||
|
|
||||||
tracker.create_effect(function()
|
tracker.create_effect(function()
|
||||||
signal:get() -- track as a dependency
|
test_signal:get() -- track as a dependency
|
||||||
call_count = call_count + 1
|
call_count = call_count + 1
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert.is.equal(call_count, 1)
|
assert.is.equal(call_count, 1)
|
||||||
signal:set(10)
|
test_signal:set(10)
|
||||||
assert.is.equal(call_count, 2)
|
assert.is.equal(call_count, 2)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('should clean up signals and not call after dispose', function()
|
it('should clean up signals and not call after dispose', function()
|
||||||
local signal = Signal:new(5)
|
local test_signal = Signal:new(5)
|
||||||
local call_count = 0
|
local call_count = 0
|
||||||
|
|
||||||
local unsubscribe = tracker.create_effect(function()
|
local unsubscribe = tracker.create_effect(function()
|
||||||
call_count = call_count + 1
|
call_count = call_count + 1
|
||||||
return signal:get() * 2
|
return test_signal:get() * 2
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert.is.equal(call_count, 1) -- Initially calls
|
assert.is.equal(call_count, 1) -- Initially calls
|
||||||
unsubscribe() -- Unsubscribe the effect
|
unsubscribe() -- Unsubscribe the effect
|
||||||
signal:set(10) -- Update signal value
|
test_signal:set(10) -- Update signal value
|
||||||
assert.is.equal(call_count, 1) -- Callback should not be called again
|
assert.is.equal(call_count, 1) -- Callback should not be called again
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
require 'luacov'
|
||||||
local function withbuf(lines, f)
|
local function withbuf(lines, f)
|
||||||
vim.go.swapfile = false
|
vim.go.swapfile = false
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
syntax = "LuaJIT"
|
||||||
call_parentheses = "None"
|
call_parentheses = "None"
|
||||||
collapse_simple_statement = "Always"
|
collapse_simple_statement = "Always"
|
||||||
column_width = 120
|
column_width = 100
|
||||||
indent_type = "Spaces"
|
indent_type = "Spaces"
|
||||||
indent_width = 2
|
indent_width = 2
|
||||||
quote_style = "AutoPreferSingle"
|
quote_style = "AutoPreferSingle"
|
||||||
|
|
||||||
|
[sort_requires]
|
||||||
|
enabled = true
|
||||||
|
|||||||
36
vim.yml
36
vim.yml
@@ -1,36 +0,0 @@
|
|||||||
---
|
|
||||||
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