Compare commits
1 Commits
x
...
d941cbb2b7
| Author | SHA1 | Date | |
|---|---|---|---|
| d941cbb2b7 |
13
.busted
13
.busted
@@ -1,13 +0,0 @@
|
|||||||
return {
|
|
||||||
_all = {
|
|
||||||
coverage = false,
|
|
||||||
lpath = "lua/?.lua;lua/?/init.lua",
|
|
||||||
lua = "nvim -u NONE -i NONE -l",
|
|
||||||
},
|
|
||||||
default = {
|
|
||||||
verbose = true
|
|
||||||
},
|
|
||||||
tests = {
|
|
||||||
verbose = true
|
|
||||||
},
|
|
||||||
}
|
|
||||||
24
.github/workflows/ci.yaml
vendored
24
.github/workflows/ci.yaml
vendored
@@ -2,28 +2,14 @@
|
|||||||
name: NeoVim tests
|
name: NeoVim tests
|
||||||
on: [push]
|
on: [push]
|
||||||
jobs:
|
jobs:
|
||||||
code-quality:
|
plenary-tests:
|
||||||
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: cachix/install-nix-action@v31
|
- uses: rhysd/action-setup-vim@v1
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
neovim: true
|
||||||
|
version: v0.10.1
|
||||||
- name: Populate Nix store
|
- run: make test
|
||||||
run:
|
|
||||||
nix-shell --pure --run 'true'
|
|
||||||
|
|
||||||
- name: Type-check with lua-language-server
|
|
||||||
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'
|
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1 @@
|
|||||||
*.src.rock
|
*.src.rock
|
||||||
*.aider*
|
|
||||||
luacov.*.out
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
-- :vim set ft=lua
|
|
||||||
globals = { "vim" }
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
when:
|
|
||||||
- event: push
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: build
|
|
||||||
image: nixos/nix
|
|
||||||
commands:
|
|
||||||
- nix-shell --pure --run 'make lint'
|
|
||||||
- nix-shell --pure --run 'make fmt-check'
|
|
||||||
- nix-shell --pure --run 'make test'
|
|
||||||
27
Makefile
27
Makefile
@@ -1,24 +1,15 @@
|
|||||||
all: lint fmt-check test
|
PLENARY_DIR=~/.local/share/nvim/site/pack/test/opt/plenary.nvim
|
||||||
|
|
||||||
|
all: lint test
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
@echo "## Typechecking"
|
selene .
|
||||||
@lua-language-server --check=lua/u/ --checklevel=Error
|
|
||||||
|
|
||||||
fmt-check:
|
|
||||||
@echo "## Checking code format"
|
|
||||||
@stylua --check .
|
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
@echo "## Formatting code"
|
stylua .
|
||||||
@stylua .
|
|
||||||
|
|
||||||
test:
|
test: $(PLENARY_DIR)
|
||||||
@rm -f luacov.*.out
|
NVIM_APPNAME=noplugstest nvim -u NORC --headless -c 'set packpath+=~/.local/share/nvim/site' -c 'packadd plenary.nvim' -c "PlenaryBustedDirectory spec/"
|
||||||
@echo "## Running tests"
|
|
||||||
@busted --coverage --verbose
|
|
||||||
@echo "## Generating coverage report"
|
|
||||||
@luacov
|
|
||||||
@awk '/^Summary$$/{flag=1;next} flag{print}' luacov.report.out
|
|
||||||
|
|
||||||
watch:
|
$(PLENARY_DIR):
|
||||||
@watchexec -c -e lua make
|
git clone https://github.com/nvim-lua/plenary.nvim/ $(PLENARY_DIR)
|
||||||
|
|||||||
162
README.md
162
README.md
@@ -1,40 +1,26 @@
|
|||||||
# u.nvim
|
# u.nvim
|
||||||
|
|
||||||
Welcome to **u.nvim** - a powerful Lua library designed to enhance your text
|
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.
|
||||||
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`
|
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.
|
||||||
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
|
- **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)
|
||||||
hyperscript into a buffer, supporting creating/managing extmarks, highlights,
|
- **Signals**: a simple dependency tracking system that pairs well with the rendering utilities for creating reactive/interactive UIs in NeoVim.
|
||||||
and key-event handling (requires NeoVim >0.11)
|
- **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).
|
||||||
- **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
|
- **Operator Key Mapping**: Flexible key mapping that works with the selected text.
|
||||||
text.
|
- **Text and Position Utilities**: Convenient functions to manage text objects and cursor positions.
|
||||||
- **Text and Position Utilities**: Convenient functions to manage text objects
|
|
||||||
and cursor positions.
|
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
This being a library, and not a proper plugin, it is recommended that you
|
lazy.nvim:
|
||||||
vendor the specific version of this library that you need, including it in your
|
```lua
|
||||||
code. Package managers are a developing landscape for Lua in the context of
|
-- Setting `lazy = true` ensures that the library is only loaded
|
||||||
NeoVim. Perhaps in the future, `lux` will eliminate the need to vendor this
|
-- when `require 'u.<utility>' is called.
|
||||||
library in your application code.
|
{ 'jrop/u.nvim', lazy = true }
|
||||||
|
```
|
||||||
|
|
||||||
## Signal and Rendering Usage
|
## Signal and Rendering Usage
|
||||||
|
|
||||||
@@ -125,9 +111,7 @@ end)
|
|||||||
|
|
||||||
### `u.tracker`
|
### `u.tracker`
|
||||||
|
|
||||||
The `u.tracker` module provides a simple API for creating reactive variables.
|
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.
|
||||||
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')
|
||||||
@@ -150,8 +134,7 @@ The renderer library renders hyperscript into a buffer. Each render performs a
|
|||||||
minimal set of changes in order to transform the current buffer text into the
|
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)
|
**Hyperscript** is just 1) _text_ 2) `<text>` tags, which can be nested in 3) Lua tables for readability:
|
||||||
Lua tables for readability:
|
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local h = require('u.renderer').h
|
local h = require('u.renderer').h
|
||||||
@@ -213,8 +196,7 @@ renderer:render(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Rendering**: The renderer library provides a `render` function that takes
|
**Rendering**: The renderer library provides a `render` function that takes hyperscript in, and converts it to formatted buffer text:
|
||||||
hyperscript in, and converts it to formatted buffer text:
|
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local Renderer = require('u.renderer').Renderer
|
local Renderer = require('u.renderer').Renderer
|
||||||
@@ -235,81 +217,49 @@ buf:render {
|
|||||||
|
|
||||||
### A note on indices
|
### A note on indices
|
||||||
|
|
||||||
<blockquote>
|
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>
|
|
||||||
I love NeoVim. I am coming to love Lua. I don't like 1-based indices; perhaps I
|
|
||||||
am too old. Perhaps I am too steeped in the history of loving the elegance of
|
|
||||||
simple pointer arithmetic. Regardless, the way positions are addressed in
|
|
||||||
NeoVim/Vim is (terrifyingly) mixed. Some methods return 1-based, others accept
|
|
||||||
only 0-based. In order to stay sane, I had to make a choice to store everything
|
|
||||||
in one, uniform representation in this library. I chose (what I humbly think is
|
|
||||||
the only sane way) to stick with the tried-and-true 0-based index scheme. That
|
|
||||||
abstraction leaks into the public API of this library.
|
|
||||||
</del>
|
|
||||||
</blockquote>
|
|
||||||
|
|
||||||
<br />
|
|
||||||
<b>This has changed in v2</b>. After much thought, I realized that:
|
|
||||||
|
|
||||||
1. The 0-based indexing in NeoVim is prevelant in the `:api`, which is designed
|
|
||||||
to be exposed to many languages. As such, it makes sense for this interface
|
|
||||||
to use 0-based indexing. However, many internal Vim functions use 1-based
|
|
||||||
indexing.
|
|
||||||
2. This is a Lua library (surprise, surprise, duh) - the idioms of the language
|
|
||||||
should take precedence over my preference
|
|
||||||
3. There were subtle bugs in the code where indices weren't being normalized to
|
|
||||||
0-based, anyways. Somehow it worked most of the time.
|
|
||||||
|
|
||||||
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
|
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.
|
||||||
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'
|
||||||
local start = Pos.new(0, 1, 1) -- Line 1, first column
|
local start = Pos.new(0, 0, 0) -- Line 1, first column
|
||||||
local stop = Pos.new(0, 3, 1) -- Line 3, first column
|
local stop = Pos.new(0, 2, 0) -- Line 3, first column
|
||||||
|
|
||||||
Range.new(start, stop, 'v') -- charwise selection
|
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
|
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".
|
||||||
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:
|
||||||
Range.from_line(bufnr, 1)
|
Range.from_line(0, 0)
|
||||||
|
|
||||||
-- Text Objects (any text object valid in your configuration is supported):
|
-- Text Objects (any text object valid in your configuration is supported):
|
||||||
-- get the word the cursor is on:
|
-- get the word the cursor is on:
|
||||||
Range.from_motion('iw')
|
Range.from_text_object('iw')
|
||||||
-- get the WORD the cursor is on:
|
-- get the WORD the cursor is on:
|
||||||
Range.from_motion('iW')
|
Range.from_text_object('iW')
|
||||||
-- get the "..." the cursor is within:
|
-- get the "..." the cursor is within:
|
||||||
Range.from_motion('a"')
|
Range.from_text_object('a"')
|
||||||
|
|
||||||
-- Get the currently visually selected text:
|
-- Get the currently visually selected text:
|
||||||
-- NOTE: this does NOT work within certain contexts; more specialized utilities
|
-- NOTE: this does NOT work within certain contexts; more specialized utilities are more appropriate in certain circumstances
|
||||||
-- are more appropriate in certain circumstances
|
|
||||||
Range.from_vtext()
|
Range.from_vtext()
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Get the operated on text obtained from a motion:
|
-- Get the operated on text obtained from a motion:
|
||||||
-- (HINT: use the opkeymap utility to make this less verbose)
|
-- (HINT: use the opkeymap utility to make this less verbose)
|
||||||
--
|
--
|
||||||
--- @param ty 'char'|'line'|'block'
|
---@param ty 'char'|'line'|'block'
|
||||||
function MyOpFunc(ty)
|
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
|
-- Try invoking this with: `<Leader>toaw`, and the current word will be the context:
|
||||||
-- 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@'
|
||||||
@@ -318,8 +268,7 @@ end, { expr = true })
|
|||||||
--
|
--
|
||||||
-- Commands:
|
-- Commands:
|
||||||
--
|
--
|
||||||
-- When executing commands in a visual context, getting the selected text has
|
-- When executing commands in a visual context, getting the selected text has to be done differently:
|
||||||
-- 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
|
||||||
@@ -330,15 +279,14 @@ 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
|
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!
|
||||||
range once you have one? Plenty, it turns out!
|
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local range = ...
|
local range = ...
|
||||||
range:lines() -- get the lines in the range's region
|
range:lines() -- get the lines in the range's region
|
||||||
range:text() -- get the text (i.e., string) in the range's region
|
range:text() -- get the text (i.e., string) in the range's region
|
||||||
range:line(1) -- get the first line within this range
|
range:line0(0) -- get the first line within this range
|
||||||
range:line(-1) -- get the last line within this range
|
range:line0(-1) -- get the last line within this range
|
||||||
-- replace with new contents:
|
-- replace with new contents:
|
||||||
range:replace {
|
range:replace {
|
||||||
'replacement line 1',
|
'replacement line 1',
|
||||||
@@ -385,11 +333,11 @@ Simply by returning a `Range` or a `Pos`, you can easily and quickly define
|
|||||||
your own text objects:
|
your own text objects:
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local txtobj = require 'u.txtobj'
|
local utils = require 'u.utils'
|
||||||
local Range = require 'u.range'
|
local Range = require 'u.range'
|
||||||
|
|
||||||
-- Select whole file:
|
-- Select whole file:
|
||||||
txtobj.define('ag', function()
|
utils.define_text_object('ag', function()
|
||||||
return Range.from_buf_text()
|
return Range.from_buf_text()
|
||||||
end)
|
end)
|
||||||
```
|
```
|
||||||
@@ -401,39 +349,27 @@ 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.b.<option> -- get buffer-local variables
|
buf:line_count() -- the number of lines in the current buffer
|
||||||
buf.b.<option> = ... -- set buffer-local variables
|
buf:get_option '...'
|
||||||
buf.bo.<option> -- get buffer options
|
buf:set_option('...', ...)
|
||||||
buf.bo.<option> = ... -- set buffer options
|
buf:get_var '...'
|
||||||
buf:line_count() -- the number of lines in the current buffer
|
buf:set_var('...', ...)
|
||||||
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:line0(0) -- returns a Range representing the first line in the buffer
|
||||||
buf:line(-1) -- returns a Range representing the last line in the buffer
|
buf:line0(-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(0, 1) -- 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(1, -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:text_object('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
|
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:
|
||||||
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
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
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.
|
||||||
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 Buffer = require 'u.buffer'
|
|
||||||
local tracker = require 'u.tracker'
|
local tracker = require 'u.tracker'
|
||||||
|
local Buffer = require 'u.buffer'
|
||||||
local h = require('u.renderer').h
|
local h = require('u.renderer').h
|
||||||
|
|
||||||
-- Create an buffer for the UI
|
-- Create an buffer for the UI
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
-- change on the underlying filesystem.
|
-- change on the underlying filesystem.
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
--- @alias u.examples.FsDir { kind: 'dir'; path: string; expanded: boolean; children: u.examples.FsNode[] }
|
--- @alias FsDir { kind: 'dir'; path: string; expanded: boolean; children: FsNode[] }
|
||||||
--- @alias u.examples.FsFile { kind: 'file'; path: string }
|
--- @alias FsFile { kind: 'file'; path: string }
|
||||||
--- @alias u.examples.FsNode u.examples.FsDir | u.examples.FsFile
|
--- @alias FsNode FsDir | FsFile
|
||||||
--- @alias u.examples.ShowOpts { root_path?: string, width?: number, focus_path?: string }
|
--- @alias ShowOpts { root_path?: string, width?: number, focus_path?: string }
|
||||||
|
|
||||||
local Buffer = require 'u.buffer'
|
local Buffer = require 'u.buffer'
|
||||||
local Renderer = require('u.renderer').Renderer
|
local Renderer = require('u.renderer').Renderer
|
||||||
@@ -44,7 +44,11 @@ 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) return vim.fs.abspath(vim.fs.normalize(path)) end
|
function H.normalize(path)
|
||||||
|
path = vim.fs.normalize(path)
|
||||||
|
if path:sub(1, 1) ~= '/' then path = vim.fs.joinpath(vim.uv.cwd(), path) end
|
||||||
|
return vim.fs.normalize(path)
|
||||||
|
end
|
||||||
|
|
||||||
--- Computes the relative path from `base` to `path`.
|
--- Computes the relative path from `base` to `path`.
|
||||||
--- @param path string
|
--- @param path string
|
||||||
@@ -58,13 +62,13 @@ function H.relative(path, base)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- @param root_path string
|
--- @param root_path string
|
||||||
--- @return { tree: u.examples.FsDir; path_to_node: table<string, u.examples.FsNode> }
|
--- @return { tree: FsDir; path_to_node: table<string, FsNode> }
|
||||||
function H.get_tree_inf(root_path)
|
function H.get_tree_inf(root_path)
|
||||||
logger:info { 'get_tree_inf', root_path }
|
logger:info { 'get_tree_inf', root_path }
|
||||||
--- @type table<string, u.examples.FsNode>
|
--- @type table<string, FsNode>
|
||||||
local path_to_node = {}
|
local path_to_node = {}
|
||||||
|
|
||||||
--- @type u.examples.FsDir
|
--- @type FsDir
|
||||||
local tree = {
|
local tree = {
|
||||||
kind = 'dir',
|
kind = 'dir',
|
||||||
path = H.normalize(root_path or '.'),
|
path = H.normalize(root_path or '.'),
|
||||||
@@ -77,8 +81,8 @@ function H.get_tree_inf(root_path)
|
|||||||
return { tree = tree, path_to_node = path_to_node }
|
return { tree = tree, path_to_node = path_to_node }
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param tree u.examples.FsDir
|
--- @param tree FsDir
|
||||||
--- @param path_to_node table<string, u.examples.FsNode>
|
--- @param path_to_node table<string, FsNode>
|
||||||
function H.populate_dir_children(tree, path_to_node)
|
function H.populate_dir_children(tree, path_to_node)
|
||||||
tree.children = {}
|
tree.children = {}
|
||||||
|
|
||||||
@@ -121,10 +125,7 @@ end
|
|||||||
---
|
---
|
||||||
--- @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(
|
local winnr = vim.api.nvim_buf_call(opts.bufnr, function() return vim.api.nvim_get_current_win() end)
|
||||||
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))
|
||||||
|
|
||||||
@@ -135,7 +136,7 @@ local function _render_in_buffer(opts)
|
|||||||
local parts = H.split_path(H.relative(focused_path, tree_inf.tree.path))
|
local parts = H.split_path(H.relative(focused_path, tree_inf.tree.path))
|
||||||
local path_to_node = tree_inf.path_to_node
|
local path_to_node = tree_inf.path_to_node
|
||||||
|
|
||||||
--- @param node u.examples.FsDir
|
--- @param node FsDir
|
||||||
--- @param child_names string[]
|
--- @param child_names string[]
|
||||||
local function expand_to(node, child_names)
|
local function expand_to(node, child_names)
|
||||||
if #child_names == 0 then return end
|
if #child_names == 0 then return end
|
||||||
@@ -154,35 +155,19 @@ 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))
|
||||||
--
|
--
|
||||||
-- :help watch-file
|
-- -- TODO: proper disposal
|
||||||
--
|
-- watcher:stop()
|
||||||
local watcher = vim.uv.new_fs_event()
|
-- end)
|
||||||
if watcher ~= nil then
|
-- end
|
||||||
--- @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 = {}
|
||||||
|
|
||||||
@@ -310,7 +295,7 @@ local function _render_in_buffer(opts)
|
|||||||
--
|
--
|
||||||
local renderer = Renderer.new(opts.bufnr)
|
local renderer = Renderer.new(opts.bufnr)
|
||||||
tracker.create_effect(function()
|
tracker.create_effect(function()
|
||||||
--- @type { tree: u.examples.FsDir; path_to_node: table<string, u.examples.FsNode> }
|
--- @type { tree: FsDir; path_to_node: table<string, FsNode> }
|
||||||
local tree_inf = s_tree_inf:get()
|
local tree_inf = s_tree_inf:get()
|
||||||
local tree = tree_inf.tree
|
local tree = tree_inf.tree
|
||||||
|
|
||||||
@@ -329,7 +314,7 @@ local function _render_in_buffer(opts)
|
|||||||
|
|
||||||
--- Since the filesystem is a recursive tree of nodes, we need to
|
--- Since the filesystem is a recursive tree of nodes, we need to
|
||||||
--- recursively render each node. This function does just that:
|
--- recursively render each node. This function does just that:
|
||||||
--- @param node u.examples.FsNode
|
--- @param node FsNode
|
||||||
--- @param level number
|
--- @param level number
|
||||||
local function render_node(node, level)
|
local function render_node(node, level)
|
||||||
local name = vim.fs.basename(node.path)
|
local name = vim.fs.basename(node.path)
|
||||||
@@ -346,11 +331,7 @@ local function _render_in_buffer(opts)
|
|||||||
return ''
|
return ''
|
||||||
end,
|
end,
|
||||||
n = function()
|
n = function()
|
||||||
vim.schedule(
|
vim.schedule(function() controller.new(node.kind == 'file' and vim.fs.dirname(node.path) or node.path) end)
|
||||||
function()
|
|
||||||
controller.new(node.kind == 'file' and vim.fs.dirname(node.path) or node.path)
|
|
||||||
end
|
|
||||||
)
|
|
||||||
return ''
|
return ''
|
||||||
end,
|
end,
|
||||||
r = function()
|
r = function()
|
||||||
@@ -370,11 +351,7 @@ 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(
|
h('text', { hl = 'Constant', nmap = nmaps }, { string.rep(' ', level), icon, ' ', name }),
|
||||||
'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
|
||||||
@@ -414,7 +391,7 @@ end
|
|||||||
local current_inf = nil
|
local current_inf = nil
|
||||||
|
|
||||||
--- Show the filetree:
|
--- Show the filetree:
|
||||||
--- @param opts? u.examples.ShowOpts
|
--- @param opts? ShowOpts
|
||||||
function M.show(opts)
|
function M.show(opts)
|
||||||
if current_inf ~= nil then return current_inf.controller end
|
if current_inf ~= nil then return current_inf.controller end
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
@@ -434,8 +411,8 @@ function M.show(opts)
|
|||||||
callback = M.hide,
|
callback = M.hide,
|
||||||
})
|
})
|
||||||
|
|
||||||
vim.wo[0][0].number = false
|
vim.wo.number = false
|
||||||
vim.wo[0][0].relativenumber = false
|
vim.wo.relativenumber = false
|
||||||
|
|
||||||
local bufnr = vim.api.nvim_get_current_buf()
|
local bufnr = vim.api.nvim_get_current_buf()
|
||||||
|
|
||||||
@@ -456,7 +433,7 @@ function M.hide()
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Toggle the filetree:
|
--- Toggle the filetree:
|
||||||
--- @param opts? u.examples.ShowOpts
|
--- @param opts? ShowOpts
|
||||||
function M.toggle(opts)
|
function M.toggle(opts)
|
||||||
if current_inf == nil then
|
if current_inf == nil then
|
||||||
M.show(opts)
|
M.show(opts)
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
-- form.lua:
|
|
||||||
--
|
|
||||||
-- This is a runnable example of a form. Open this file in Neovim, and execute
|
|
||||||
-- `:luafile %` to run it. It will create a new buffer to the side, and render
|
|
||||||
-- an interactive form. Edit the "inputs" between the `[...]` brackets, and
|
|
||||||
-- watch the buffer react immediately to your changes.
|
|
||||||
--
|
|
||||||
|
|
||||||
local Renderer = require('u.renderer').Renderer
|
|
||||||
local h = require('u.renderer').h
|
|
||||||
local tracker = require 'u.tracker'
|
|
||||||
|
|
||||||
-- Create a new, temporary, buffer to the side:
|
|
||||||
vim.cmd.vnew()
|
|
||||||
vim.bo.buftype = 'nofile'
|
|
||||||
vim.bo.bufhidden = 'wipe'
|
|
||||||
vim.bo.buflisted = false
|
|
||||||
local renderer = Renderer.new()
|
|
||||||
|
|
||||||
-- Create two signals:
|
|
||||||
local s_name = tracker.create_signal 'whoever-you-are'
|
|
||||||
local s_age = tracker.create_signal 'ideally-a-number'
|
|
||||||
|
|
||||||
-- We can create derived information from the signals above. Say we want to do
|
|
||||||
-- some validation on the input for `age`: we can do that with a memo:
|
|
||||||
local s_age_info = tracker.create_memo(function()
|
|
||||||
local age_raw = s_age:get()
|
|
||||||
local age_digits = age_raw:match '^%s*(%d+)%s*$'
|
|
||||||
local age_n = age_digits and tonumber(age_digits) or nil
|
|
||||||
return {
|
|
||||||
type = age_n and 'number' or 'string',
|
|
||||||
raw = age_raw,
|
|
||||||
n = age_n,
|
|
||||||
n1 = age_n and age_n + 1 or nil,
|
|
||||||
}
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- This is the render effect that depends on the signals created above. This
|
|
||||||
-- will re-run every time one of the signals changes.
|
|
||||||
tracker.create_effect(function()
|
|
||||||
local name = s_name:get()
|
|
||||||
local age = s_age:get()
|
|
||||||
local age_info = s_age_info:get()
|
|
||||||
|
|
||||||
-- Each time the signals change, we re-render the buffer:
|
|
||||||
renderer:render {
|
|
||||||
h.Type({}, '# Form Example'),
|
|
||||||
'\n\n',
|
|
||||||
|
|
||||||
-- We can also listen for when specific locations in the buffer change, on
|
|
||||||
-- a tag-by-tag basis. This gives us two-way data-binding between the
|
|
||||||
-- buffer and the signals.
|
|
||||||
{
|
|
||||||
'Name: ',
|
|
||||||
h.Structure({
|
|
||||||
on_change = function(text) s_name:set(text) end,
|
|
||||||
}, name),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'\nAge: ',
|
|
||||||
h.Structure({
|
|
||||||
on_change = function(text) s_age:set(text) end,
|
|
||||||
}, age),
|
|
||||||
},
|
|
||||||
|
|
||||||
'\n\n',
|
|
||||||
|
|
||||||
-- Show the values of the signals here, too, so that we can see the
|
|
||||||
-- reactivity in action. If you change the values in the tags above, you
|
|
||||||
-- can see the changes reflected here immediately.
|
|
||||||
{ 'Hello, "', name, '"!' },
|
|
||||||
|
|
||||||
--
|
|
||||||
-- A more complex example: we can do much more complex rendering, based on
|
|
||||||
-- the state. For example, if you type different values into the `age`
|
|
||||||
-- field, you can see not only the displayed information change, but also
|
|
||||||
-- the color of the highlights in this section will adapt to the type of
|
|
||||||
-- information that has been detected.
|
|
||||||
--
|
|
||||||
-- If string input is detected, values below are shown in the
|
|
||||||
-- `String`/`ErrorMsg` highlight groups.
|
|
||||||
--
|
|
||||||
-- If number input is detected, values below are shown in the `Number`
|
|
||||||
-- highlight group.
|
|
||||||
--
|
|
||||||
-- If a valid number is entered, then this section also displays how old
|
|
||||||
-- you willl be next year (`n + 1`).
|
|
||||||
--
|
|
||||||
|
|
||||||
'\n\n',
|
|
||||||
h.Type({}, '## Computed Information (derived from `age`)'),
|
|
||||||
'\n\n',
|
|
||||||
{
|
|
||||||
'Type: ',
|
|
||||||
h('text', {
|
|
||||||
hl = age_info.type == 'number' and 'Number' or 'String',
|
|
||||||
}, age_info.type),
|
|
||||||
},
|
|
||||||
{ '\nRaw input: ', h.String({}, '"' .. age_info.raw .. '"') },
|
|
||||||
{
|
|
||||||
'\nCurrent age: ',
|
|
||||||
age_info.n
|
|
||||||
-- Show the age:
|
|
||||||
and h.Number({}, tostring(age_info.n))
|
|
||||||
-- Show an error-placeholder if the age is invalid:
|
|
||||||
or h.ErrorMsg({}, '(?)'),
|
|
||||||
},
|
|
||||||
|
|
||||||
-- This part is shown conditionally, i.e., only if the age next year can be
|
|
||||||
-- computed:
|
|
||||||
age_info.n1
|
|
||||||
and {
|
|
||||||
'\nAge next year: ',
|
|
||||||
h.Number({}, tostring(age_info.n1)),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
end)
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
local Renderer = require('u.renderer').Renderer
|
local Buffer = require 'u.buffer'
|
||||||
local TreeBuilder = require('u.renderer').TreeBuilder
|
local TreeBuilder = require('u.renderer').TreeBuilder
|
||||||
local tracker = require 'u.tracker'
|
local tracker = require 'u.tracker'
|
||||||
local utils = require 'u.utils'
|
local utils = require 'u.utils'
|
||||||
|
local Window = require 'my.window'
|
||||||
|
|
||||||
local TIMEOUT = 4000
|
local TIMEOUT = 4000
|
||||||
local ICONS = {
|
local ICONS = {
|
||||||
@@ -13,25 +14,15 @@ local ICONS = {
|
|||||||
}
|
}
|
||||||
local DEFAULT_ICON = { text = '', group = 'DiagnosticSignOk' }
|
local DEFAULT_ICON = { text = '', group = 'DiagnosticSignOk' }
|
||||||
|
|
||||||
local S_EDITOR_DIMENSIONS =
|
--- @alias Notification {
|
||||||
tracker.create_signal(utils.get_editor_dimensions(), 's:editor_dimensions')
|
|
||||||
vim.api.nvim_create_autocmd('VimResized', {
|
|
||||||
callback = function()
|
|
||||||
local new_dim = utils.get_editor_dimensions()
|
|
||||||
S_EDITOR_DIMENSIONS:set(new_dim)
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
--- @alias u.examples.Notification {
|
|
||||||
--- kind: number;
|
--- kind: number;
|
||||||
--- id: number;
|
--- id: number;
|
||||||
--- text: string;
|
--- text: string;
|
||||||
--- timer: uv.uv_timer_t;
|
|
||||||
--- }
|
--- }
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
--- @type { win: integer, buf: integer, renderer: u.renderer.Renderer } | nil
|
--- @type Window | nil
|
||||||
local notifs_w
|
local notifs_w
|
||||||
|
|
||||||
local s_notifications_raw = tracker.create_signal {}
|
local s_notifications_raw = tracker.create_signal {}
|
||||||
@@ -39,49 +30,44 @@ local s_notifications = s_notifications_raw:debounce(50)
|
|||||||
|
|
||||||
-- Render effect:
|
-- Render effect:
|
||||||
tracker.create_effect(function()
|
tracker.create_effect(function()
|
||||||
--- @type u.examples.Notification[]
|
--- @type Notification[]
|
||||||
local notifs = s_notifications:get()
|
local notifs = s_notifications:get()
|
||||||
--- @type { width: integer, height: integer }
|
|
||||||
local editor_size = S_EDITOR_DIMENSIONS:get()
|
|
||||||
|
|
||||||
if #notifs == 0 then
|
if #notifs == 0 then
|
||||||
if notifs_w then
|
if notifs_w then
|
||||||
if vim.api.nvim_win_is_valid(notifs_w.win) then vim.api.nvim_win_close(notifs_w.win, true) end
|
notifs_w:close(true)
|
||||||
notifs_w = nil
|
notifs_w = nil
|
||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local avail_width = editor_size.width
|
|
||||||
local float_width = 40
|
|
||||||
local float_height = math.min(#notifs, editor_size.height - 3)
|
|
||||||
local win_config = {
|
|
||||||
relative = 'editor',
|
|
||||||
anchor = 'NE',
|
|
||||||
row = 0,
|
|
||||||
col = avail_width,
|
|
||||||
width = float_width,
|
|
||||||
height = float_height,
|
|
||||||
border = 'single',
|
|
||||||
focusable = false,
|
|
||||||
zindex = 900,
|
|
||||||
}
|
|
||||||
vim.schedule(function()
|
vim.schedule(function()
|
||||||
|
local editor_size = utils.get_editor_dimensions()
|
||||||
|
local avail_width = editor_size.width
|
||||||
|
local float_width = 40
|
||||||
|
local win_config = {
|
||||||
|
relative = 'editor',
|
||||||
|
anchor = 'NE',
|
||||||
|
row = 0,
|
||||||
|
col = avail_width,
|
||||||
|
width = float_width,
|
||||||
|
height = math.min(#notifs, editor_size.height - 3),
|
||||||
|
border = 'single',
|
||||||
|
focusable = false,
|
||||||
|
}
|
||||||
if not notifs_w or not vim.api.nvim_win_is_valid(notifs_w.win) then
|
if not notifs_w or not vim.api.nvim_win_is_valid(notifs_w.win) then
|
||||||
local b = vim.api.nvim_create_buf(false, true)
|
notifs_w = Window.new(Buffer.create(false, true), win_config)
|
||||||
local w = vim.api.nvim_open_win(b, false, win_config)
|
vim.wo[notifs_w.win].cursorline = false
|
||||||
vim.wo[w].cursorline = false
|
vim.wo[notifs_w.win].list = false
|
||||||
vim.wo[w].list = false
|
vim.wo[notifs_w.win].listchars = ''
|
||||||
vim.wo[w].listchars = ''
|
vim.wo[notifs_w.win].number = false
|
||||||
vim.wo[w].number = false
|
vim.wo[notifs_w.win].relativenumber = false
|
||||||
vim.wo[w].relativenumber = false
|
vim.wo[notifs_w.win].wrap = false
|
||||||
vim.wo[w].wrap = false
|
|
||||||
notifs_w = { win = w, buf = b, renderer = Renderer.new(b) }
|
|
||||||
else
|
else
|
||||||
vim.api.nvim_win_set_config(notifs_w.win, win_config)
|
notifs_w:set_config(win_config)
|
||||||
end
|
end
|
||||||
|
|
||||||
notifs_w.renderer:render(TreeBuilder.new()
|
notifs_w:render(TreeBuilder.new()
|
||||||
:nest(function(tb)
|
:nest(function(tb)
|
||||||
for idx, notif in ipairs(notifs) do
|
for idx, notif in ipairs(notifs) do
|
||||||
if idx > 1 then tb:put '\n' end
|
if idx > 1 then tb:put '\n' end
|
||||||
@@ -93,81 +79,48 @@ tracker.create_effect(function()
|
|||||||
end)
|
end)
|
||||||
:tree())
|
:tree())
|
||||||
vim.api.nvim_win_call(notifs_w.win, function()
|
vim.api.nvim_win_call(notifs_w.win, function()
|
||||||
vim.fn.winrestview {
|
-- scroll to bottom:
|
||||||
-- scroll all the way left:
|
vim.cmd.normal 'G'
|
||||||
leftcol = 0,
|
-- scroll all the way to the left:
|
||||||
-- set the bottom line to be at the bottom of the window:
|
vim.cmd.normal '9999zh'
|
||||||
topline = vim.api.nvim_buf_line_count(notifs_w.buf) - win_config.height + 1,
|
|
||||||
}
|
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
--- @param id number
|
|
||||||
local function _delete_notif(id)
|
|
||||||
--- @param notifs u.examples.Notification[]
|
|
||||||
s_notifications_raw:schedule_update(function(notifs)
|
|
||||||
for i, notif in ipairs(notifs) do
|
|
||||||
if notif.id == id then
|
|
||||||
notif.timer:stop()
|
|
||||||
notif.timer:close()
|
|
||||||
table.remove(notifs, i)
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return notifs
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local _orig_notify
|
local _orig_notify
|
||||||
|
|
||||||
--- @param msg string
|
--- @param msg string
|
||||||
--- @param level integer|nil
|
--- @param level integer|nil
|
||||||
--- @param opts? { id: number }
|
--- @param opts table|nil
|
||||||
function M.notify(msg, level, opts)
|
local function my_notify(msg, level, opts)
|
||||||
|
vim.schedule(function() _orig_notify(msg, level, opts) end)
|
||||||
if level == nil then level = vim.log.levels.INFO end
|
if level == nil then level = vim.log.levels.INFO end
|
||||||
|
if level < vim.log.levels.INFO then return end
|
||||||
|
|
||||||
opts = opts or {}
|
local id = math.random(math.huge)
|
||||||
local id = opts.id or math.random(999999999)
|
|
||||||
|
|
||||||
--- @type u.examples.Notification?
|
--- @param notifs Notification[]
|
||||||
local notif = vim.iter(s_notifications_raw:get()):find(function(n) return n.id == id end)
|
s_notifications_raw:schedule_update(function(notifs)
|
||||||
if not notif then
|
table.insert(notifs, { kind = level, id = id, text = msg })
|
||||||
-- Create a new notification (maybe):
|
return notifs
|
||||||
if vim.trim(msg) == '' then return id end
|
end)
|
||||||
if level < vim.log.levels.INFO then return id end
|
|
||||||
|
|
||||||
local timer = assert((vim.uv or vim.loop).new_timer(), 'could not create timer')
|
vim.defer_fn(function()
|
||||||
timer:start(TIMEOUT, 0, function() _delete_notif(id) end)
|
--- @param notifs Notification[]
|
||||||
notif = {
|
|
||||||
id = id,
|
|
||||||
kind = level,
|
|
||||||
text = msg,
|
|
||||||
timer = timer,
|
|
||||||
}
|
|
||||||
--- @param notifs u.examples.Notification[]
|
|
||||||
s_notifications_raw:schedule_update(function(notifs)
|
s_notifications_raw:schedule_update(function(notifs)
|
||||||
table.insert(notifs, notif)
|
for i, notif in ipairs(notifs) do
|
||||||
|
if notif.id == id then
|
||||||
|
table.remove(notifs, i)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
return notifs
|
return notifs
|
||||||
end)
|
end)
|
||||||
else
|
end, TIMEOUT)
|
||||||
-- Update an existing notification:
|
|
||||||
s_notifications_raw:schedule_update(function(notifs)
|
|
||||||
-- We already have a copy-by-reference of the notif we want to modify:
|
|
||||||
notif.timer:stop()
|
|
||||||
notif.text = msg
|
|
||||||
notif.kind = level
|
|
||||||
notif.timer:start(TIMEOUT, 0, function() _delete_notif(id) end)
|
|
||||||
|
|
||||||
return notifs
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
return id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local _once_msgs = {}
|
local _once_msgs = {}
|
||||||
function M.notify_once(msg, level, opts)
|
local function my_notify_once(msg, level, opts)
|
||||||
if vim.tbl_contains(_once_msgs, msg) then return false end
|
if vim.tbl_contains(_once_msgs, msg) then return false end
|
||||||
table.insert(_once_msgs, msg)
|
table.insert(_once_msgs, msg)
|
||||||
vim.notify(msg, level, opts)
|
vim.notify(msg, level, opts)
|
||||||
@@ -177,8 +130,8 @@ end
|
|||||||
function M.setup()
|
function M.setup()
|
||||||
if _orig_notify == nil then _orig_notify = vim.notify end
|
if _orig_notify == nil then _orig_notify = vim.notify end
|
||||||
|
|
||||||
vim.notify = M.notify
|
vim.notify = my_notify
|
||||||
vim.notify_once = M.notify_once
|
vim.notify_once = my_notify_once
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
local Buffer = require 'u.buffer'
|
|
||||||
local utils = require 'u.utils'
|
local utils = require 'u.utils'
|
||||||
|
local Buffer = require 'u.buffer'
|
||||||
local Renderer = require('u.renderer').Renderer
|
local Renderer = require('u.renderer').Renderer
|
||||||
local h = require('u.renderer').h
|
local h = require('u.renderer').h
|
||||||
local TreeBuilder = require('u.renderer').TreeBuilder
|
local TreeBuilder = require('u.renderer').TreeBuilder
|
||||||
@@ -7,8 +7,7 @@ local tracker = require 'u.tracker'
|
|||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
local S_EDITOR_DIMENSIONS =
|
local S_EDITOR_DIMENSIONS = tracker.create_signal(utils.get_editor_dimensions(), '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()
|
||||||
@@ -17,8 +16,8 @@ vim.api.nvim_create_autocmd('VimResized', {
|
|||||||
})
|
})
|
||||||
|
|
||||||
--- @param low number
|
--- @param low number
|
||||||
--- @param x number
|
---@param x number
|
||||||
--- @param high number
|
---@param high number
|
||||||
local function clamp(low, x, high)
|
local function clamp(low, x, high)
|
||||||
x = math.max(low, x)
|
x = math.max(low, x)
|
||||||
x = math.min(x, high)
|
x = math.min(x, high)
|
||||||
@@ -44,7 +43,7 @@ local function shallow_copy_arr(arr) return vim.iter(arr):totable() end
|
|||||||
-- shortest portion of this function.
|
-- shortest portion of this function.
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
--- @alias u.examples.SelectController {
|
--- @alias SelectController {
|
||||||
--- get_items: fun(): T[];
|
--- get_items: fun(): T[];
|
||||||
--- set_items: fun(items: T[]);
|
--- set_items: fun(items: T[]);
|
||||||
--- set_filter_text: fun(filter_text: string);
|
--- set_filter_text: fun(filter_text: string);
|
||||||
@@ -53,18 +52,18 @@ local function shallow_copy_arr(arr) return vim.iter(arr):totable() end
|
|||||||
--- set_selected_indices: fun(indicies: number[], ephemeral?: boolean);
|
--- set_selected_indices: fun(indicies: number[], ephemeral?: boolean);
|
||||||
--- close: fun();
|
--- close: fun();
|
||||||
--- }
|
--- }
|
||||||
--- @alias u.examples.SelectOpts<T> {
|
--- @alias SelectOpts<T> {
|
||||||
--- items: `T`[];
|
--- items: `T`[];
|
||||||
--- multi?: boolean;
|
--- multi?: boolean;
|
||||||
--- format_item?: fun(item: T): u.renderer.Tree;
|
--- format_item?: fun(item: T): Tree;
|
||||||
--- on_finish?: fun(items: T[], indicies: number[]);
|
--- on_finish?: fun(items: T[], indicies: number[]);
|
||||||
--- on_selection_changed?: fun(items: T[], indicies: number[]);
|
--- on_selection_changed?: fun(items: T[], indicies: number[]);
|
||||||
--- mappings?: table<string, fun(select: u.examples.SelectController)>;
|
--- mappings?: table<string, fun(select: SelectController)>;
|
||||||
--- }
|
--- }
|
||||||
|
|
||||||
--- @generic T
|
--- @generic T
|
||||||
--- @param opts u.examples.SelectOpts<T>
|
--- @param opts SelectOpts<T>
|
||||||
function M.create_picker(opts) -- {{{
|
function M.create_picker(opts)
|
||||||
local is_in_insert_mode = vim.api.nvim_get_mode().mode:sub(1, 1) == 'i'
|
local is_in_insert_mode = vim.api.nvim_get_mode().mode:sub(1, 1) == 'i'
|
||||||
local stopinsert = not is_in_insert_mode
|
local stopinsert = not is_in_insert_mode
|
||||||
|
|
||||||
@@ -146,19 +145,17 @@ function M.create_picker(opts) -- {{{
|
|||||||
col = s_w_input_coords:get().col,
|
col = s_w_input_coords:get().col,
|
||||||
relative = 'editor',
|
relative = 'editor',
|
||||||
focusable = true,
|
focusable = true,
|
||||||
border = vim.o.winborder or 'rounded',
|
border = 'rounded',
|
||||||
}
|
}
|
||||||
local w_input_buf = Buffer.create(false, true)
|
local w_input_buf = Buffer.create(false, true)
|
||||||
local w_input = vim.api.nvim_open_win(w_input_buf.bufnr, false, w_input_cfg)
|
local w_input = vim.api.nvim_open_win(w_input_buf.buf, false, w_input_cfg)
|
||||||
vim.wo[w_input][0].cursorline = false
|
vim.wo[w_input].number = false
|
||||||
vim.wo[w_input][0].list = false
|
vim.wo[w_input].relativenumber = false
|
||||||
vim.wo[w_input][0].number = false
|
|
||||||
vim.wo[w_input][0].relativenumber = false
|
|
||||||
|
|
||||||
-- The following option is a signal to other plugins like 'cmp' to not mess
|
-- The following option is a signal to other plugins like 'cmp' to not mess
|
||||||
-- with this buffer:
|
-- with this buffer:
|
||||||
vim.bo[w_input_buf.bufnr].buftype = 'prompt'
|
vim.bo[w_input_buf.buf].buftype = 'prompt'
|
||||||
vim.fn.prompt_setprompt(w_input_buf.bufnr, '')
|
vim.fn.prompt_setprompt(w_input_buf.buf, '')
|
||||||
|
|
||||||
vim.api.nvim_set_current_win(w_input)
|
vim.api.nvim_set_current_win(w_input)
|
||||||
tracker.create_effect(safe_wrap(function()
|
tracker.create_effect(safe_wrap(function()
|
||||||
@@ -177,10 +174,10 @@ function M.create_picker(opts) -- {{{
|
|||||||
border = 'rounded',
|
border = 'rounded',
|
||||||
}
|
}
|
||||||
local w_list_buf = Buffer.create(false, true)
|
local w_list_buf = Buffer.create(false, true)
|
||||||
local w_list = vim.api.nvim_open_win(w_list_buf.bufnr, false, w_list_cfg)
|
local w_list = vim.api.nvim_open_win(w_list_buf.buf, false, w_list_cfg)
|
||||||
vim.wo[w_list][0].number = false
|
vim.wo[w_list].number = false
|
||||||
vim.wo[w_list][0].relativenumber = false
|
vim.wo[w_list].relativenumber = false
|
||||||
vim.wo[w_list][0].scrolloff = 0
|
vim.wo[w_list].scrolloff = 0
|
||||||
tracker.create_effect(safe_wrap(function()
|
tracker.create_effect(safe_wrap(function()
|
||||||
-- update window position/size every time the editor is resized:
|
-- update window position/size every time the editor is resized:
|
||||||
w_list_cfg = vim.tbl_deep_extend('force', w_list_cfg, s_w_list_coords:get())
|
w_list_cfg = vim.tbl_deep_extend('force', w_list_cfg, s_w_list_coords:get())
|
||||||
@@ -203,9 +200,7 @@ 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(
|
callback = safe_wrap(function() s_filter_text_undebounced:set(vim.api.nvim_get_current_line()) end),
|
||||||
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)
|
||||||
|
|
||||||
@@ -214,15 +209,10 @@ 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)
|
local function _format_item(item) return opts.format_item and opts.format_item(item) or tostring(item) end
|
||||||
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
|
return vim.iter(items):map(function(item) return { item = item, formatted = _format_item(item) } end):totable()
|
||||||
.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:
|
||||||
@@ -231,21 +221,13 @@ 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()
|
||||||
|
|
||||||
--- @type string
|
local filter_pattern = ''
|
||||||
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:gsub('%.', '%%.')
|
||||||
use_plain_pattern = true
|
|
||||||
elseif #formatted_items > 1000 then
|
|
||||||
filter_pattern = filter_text
|
|
||||||
use_plain_pattern = true
|
|
||||||
else
|
else
|
||||||
filter_pattern = '('
|
filter_pattern = '('
|
||||||
.. vim.iter(vim.split(filter_text, '')):map(function(c) return c .. '.*' end):join ''
|
.. vim.iter(vim.split(filter_text, '')):map(function(c) return c .. '.*' end):join ''
|
||||||
.. ')'
|
.. ')'
|
||||||
use_plain_pattern = false
|
|
||||||
end
|
end
|
||||||
filter_pattern = filter_pattern:lower()
|
filter_pattern = filter_pattern:lower()
|
||||||
|
|
||||||
@@ -257,20 +239,13 @@ function M.create_picker(opts) -- {{{
|
|||||||
local new_filtered_items = vim
|
local new_filtered_items = vim
|
||||||
.iter(formatted_items)
|
.iter(formatted_items)
|
||||||
:enumerate()
|
:enumerate()
|
||||||
:map(
|
:map(function(i, inf) return { orig_idx = i, item = inf.item, formatted = inf.formatted } end)
|
||||||
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()
|
||||||
|
|
||||||
formatted_strings[inf.orig_idx] = formatted_as_string
|
formatted_strings[inf.orig_idx] = formatted_as_string
|
||||||
if use_plain_pattern then
|
matches[inf.orig_idx] = string.match(formatted_as_string, filter_pattern)
|
||||||
local x, y = formatted_as_string:find(filter_pattern, 1, true)
|
|
||||||
if x ~= nil and y ~= nil then matches[inf.orig_idx] = formatted_as_string:sub(x, y) end
|
|
||||||
else
|
|
||||||
matches[inf.orig_idx] = string.match(formatted_as_string, filter_pattern)
|
|
||||||
end
|
|
||||||
|
|
||||||
return matches[inf.orig_idx] ~= nil
|
return matches[inf.orig_idx] ~= nil
|
||||||
end)
|
end)
|
||||||
@@ -330,9 +305,7 @@ function M.create_picker(opts) -- {{{
|
|||||||
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
|
if #indices == 0 and #filtered_items > 0 then indices = { filtered_items[cursor_index].orig_idx } end
|
||||||
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,
|
||||||
@@ -356,7 +329,7 @@ function M.create_picker(opts) -- {{{
|
|||||||
-- "invalid window ID" errors):
|
-- "invalid window ID" errors):
|
||||||
H.unsubscribe_render_effect()
|
H.unsubscribe_render_effect()
|
||||||
-- buftype=prompt buffers are not "temporary", so delete the buffer manually:
|
-- buftype=prompt buffers are not "temporary", so delete the buffer manually:
|
||||||
vim.api.nvim_buf_delete(w_input_buf.bufnr, { force = true })
|
vim.api.nvim_buf_delete(w_input_buf.buf, { force = true })
|
||||||
-- The following is not needed, since the buffer is deleted above:
|
-- The following is not needed, since the buffer is deleted above:
|
||||||
-- vim.api.nvim_win_close(w_input, false)
|
-- vim.api.nvim_win_close(w_input, false)
|
||||||
vim.api.nvim_win_close(w_list, false)
|
vim.api.nvim_win_close(w_list, false)
|
||||||
@@ -397,9 +370,9 @@ function M.create_picker(opts) -- {{{
|
|||||||
--
|
--
|
||||||
-- Events
|
-- Events
|
||||||
--
|
--
|
||||||
vim.keymap.set('i', '<Esc>', function() H.finish(true) end, { buffer = w_input_buf.bufnr })
|
vim.keymap.set('i', '<Esc>', function() H.finish(true) end, { buffer = w_input_buf.buf })
|
||||||
|
|
||||||
vim.keymap.set('i', '<CR>', function() H.finish() end, { buffer = w_input_buf.bufnr })
|
vim.keymap.set('i', '<CR>', function() H.finish() end, { buffer = w_input_buf.buf })
|
||||||
|
|
||||||
local function action_next_line()
|
local function action_next_line()
|
||||||
local max_line = #s_filtered_items:get()
|
local max_line = #s_filtered_items:get()
|
||||||
@@ -409,18 +382,8 @@ 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(
|
vim.keymap.set('i', '<C-n>', safe_wrap(action_next_line), { buffer = w_input_buf.buf, desc = 'Picker: next' })
|
||||||
'i',
|
vim.keymap.set('i', '<Down>', safe_wrap(action_next_line), { buffer = w_input_buf.buf, desc = 'Picker: next' })
|
||||||
'<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()
|
||||||
@@ -428,18 +391,8 @@ function M.create_picker(opts) -- {{{
|
|||||||
if next_cursor_index - s_top_offset:get() < 1 then s_top_offset:set(s_top_offset:get() - 1) end
|
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(
|
vim.keymap.set('i', '<C-p>', safe_wrap(action_prev_line), { buffer = w_input_buf.buf, desc = 'Picker: previous' })
|
||||||
'i',
|
vim.keymap.set('i', '<Up>', safe_wrap(action_prev_line), { buffer = w_input_buf.buf, desc = 'Picker: previous' })
|
||||||
'<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',
|
||||||
@@ -449,9 +402,7 @@ 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(
|
s_selected_indices:set(vim.iter(s_selected_indices:get()):filter(function(i) return i ~= index end):totable())
|
||||||
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)
|
||||||
@@ -459,16 +410,11 @@ function M.create_picker(opts) -- {{{
|
|||||||
end
|
end
|
||||||
action_next_line()
|
action_next_line()
|
||||||
end),
|
end),
|
||||||
{ buffer = w_input_buf.bufnr }
|
{ buffer = w_input_buf.buf }
|
||||||
)
|
)
|
||||||
|
|
||||||
for key, fn in pairs(opts.mappings or {}) do
|
for key, fn in pairs(opts.mappings or {}) do
|
||||||
vim.keymap.set(
|
vim.keymap.set('i', key, safe_wrap(function() return fn(controller) end), { buffer = w_input_buf.buf })
|
||||||
'i',
|
|
||||||
key,
|
|
||||||
safe_wrap(function() return fn(controller) end),
|
|
||||||
{ buffer = w_input_buf.bufnr }
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Render:
|
-- Render:
|
||||||
@@ -512,7 +458,7 @@ function M.create_picker(opts) -- {{{
|
|||||||
return safe_run(function() return s_items_raw:get() end)
|
return safe_run(function() return s_items_raw:get() end)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param items T[]
|
---@param items T[]
|
||||||
function controller.set_items(items)
|
function controller.set_items(items)
|
||||||
return safe_run(function() s_items_raw:set(items) end)
|
return safe_run(function() s_items_raw:set(items) end)
|
||||||
end
|
end
|
||||||
@@ -532,15 +478,16 @@ function M.create_picker(opts) -- {{{
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- @param indicies number[]
|
--- @param indicies number[]
|
||||||
--- @param ephemeral? boolean
|
---@param ephemeral? boolean
|
||||||
function controller.set_selected_indices(indicies, ephemeral)
|
function controller.set_selected_indices(indicies, ephemeral)
|
||||||
return safe_run(function()
|
return safe_run(function()
|
||||||
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.iter(s_filtered_items:get()):enumerate():find(
|
local matching_filtered_item_idx, _ = vim
|
||||||
function(_idx, inf) return inf.orig_idx == indicies[1] end
|
.iter(s_filtered_items:get())
|
||||||
)
|
:enumerate()
|
||||||
|
:find(function(_idx, inf) return inf.orig_idx == indicies[1] end)
|
||||||
if matching_filtered_item_idx ~= nil then s_cursor_index:set(indicies[1]) end
|
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
|
||||||
@@ -557,8 +504,8 @@ function M.create_picker(opts) -- {{{
|
|||||||
return safe_run(function() H.finish(true) end)
|
return safe_run(function() H.finish(true) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return controller --[[@as u.examples.SelectController]]
|
return controller --[[@as SelectController]]
|
||||||
end -- }}}
|
end
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- END create_picker
|
-- END create_picker
|
||||||
@@ -575,7 +522,6 @@ function M.register_ui_select()
|
|||||||
--- @param items `T`[]
|
--- @param items `T`[]
|
||||||
--- @param opts { prompt?: string, kind?: any, format_item?: fun(item: T):string }
|
--- @param opts { prompt?: string, kind?: any, format_item?: fun(item: T):string }
|
||||||
--- @param cb fun(item: T|nil):any
|
--- @param cb fun(item: T|nil):any
|
||||||
--- @diagnostic disable-next-line: duplicate-set-field
|
|
||||||
function vim.ui.select(items, opts, cb)
|
function vim.ui.select(items, opts, cb)
|
||||||
M.create_picker {
|
M.create_picker {
|
||||||
items = items,
|
items = items,
|
||||||
@@ -606,7 +552,7 @@ end
|
|||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
--- @param opts? { limit?: number }
|
--- @param opts? { limit?: number }
|
||||||
function M.files(opts) -- {{{
|
function M.files(opts)
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
opts.limit = opts.limit or 10000
|
opts.limit = opts.limit or 10000
|
||||||
|
|
||||||
@@ -759,11 +705,7 @@ 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(
|
vim.notify('Picker list is too large (truncating list to ' .. opts.limit .. ' items)', vim.log.levels.WARN)
|
||||||
'Picker list is too large (truncating list to ' .. opts.limit .. ' items)',
|
|
||||||
vim.log.levels.WARN
|
|
||||||
)
|
|
||||||
pcall(vim.fn.jobstop, job_inf.id)
|
|
||||||
job_inf.notified_over_limit = true
|
job_inf.notified_over_limit = true
|
||||||
end
|
end
|
||||||
return
|
return
|
||||||
@@ -784,10 +726,10 @@ function M.files(opts) -- {{{
|
|||||||
set_lines_as_items_state()
|
set_lines_as_items_state()
|
||||||
end),
|
end),
|
||||||
})
|
})
|
||||||
end -- }}}
|
end
|
||||||
|
|
||||||
function M.buffers() -- {{{
|
function M.buffers()
|
||||||
local cwd = vim.fn.getcwd()
|
local cwd = vim.fn.getcwd(0, 0)
|
||||||
-- 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
|
||||||
|
|
||||||
@@ -805,7 +747,10 @@ function M.buffers() -- {{{
|
|||||||
-- 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():put(item.changed == 1 and '[+] ' or ' '):put(item_name):tree()
|
return TreeBuilder.new()
|
||||||
|
:put(item.changed == 1 and '[+] ' or ' ')
|
||||||
|
:put(item_name)
|
||||||
|
:tree()
|
||||||
end,
|
end,
|
||||||
|
|
||||||
--- @params items { bufnr: number }[]
|
--- @params items { bufnr: number }[]
|
||||||
@@ -886,10 +831,10 @@ function M.buffers() -- {{{
|
|||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
end -- }}}
|
end
|
||||||
|
|
||||||
local IS_CODE_SYMBOL_RUNNING = false
|
local IS_CODE_SYMBOL_RUNNING = false
|
||||||
function M.lsp_code_symbols() -- {{{
|
function M.lsp_code_symbols()
|
||||||
if IS_CODE_SYMBOL_RUNNING then return end
|
if IS_CODE_SYMBOL_RUNNING then return end
|
||||||
IS_CODE_SYMBOL_RUNNING = true
|
IS_CODE_SYMBOL_RUNNING = true
|
||||||
|
|
||||||
@@ -918,9 +863,7 @@ 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
|
local buf = vim.iter(vim.fn.getbufinfo { buflisted = 1 }):find(function(b) return b.name == item.filename end)
|
||||||
.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
|
||||||
@@ -938,12 +881,13 @@ function M.lsp_code_symbols() -- {{{
|
|||||||
|
|
||||||
-- Kick off the async operation:
|
-- Kick off the async operation:
|
||||||
vim.lsp.buf.document_symbol { on_list = STEPS._1_on_symbols }
|
vim.lsp.buf.document_symbol { on_list = STEPS._1_on_symbols }
|
||||||
end -- }}}
|
end
|
||||||
|
|
||||||
function M.setup()
|
function M.setup()
|
||||||
utils.ucmd('Files', M.files)
|
utils.ucmd('Files', M.files)
|
||||||
utils.ucmd('Buffers', M.buffers)
|
utils.ucmd('Buffers', M.buffers)
|
||||||
utils.ucmd('Lspcodesymbols', M.lsp_code_symbols)
|
utils.ucmd('Lspcodesymbols', M.lsp_code_symbols)
|
||||||
|
M.register_ui_select()
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
|
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 = {}
|
||||||
|
|
||||||
--- @param bracket_range u.Range
|
---@param bracket_range Range
|
||||||
--- @param left string
|
---@param left string
|
||||||
--- @param right string
|
---@param right string
|
||||||
local function split(bracket_range, left, right)
|
local function split(bracket_range, left, right)
|
||||||
local code = CodeWriter.from_pos(bracket_range.start)
|
local code = CodeWriter.from_pos(bracket_range.start)
|
||||||
code:write_raw(left)
|
code:write_raw(left)
|
||||||
@@ -52,21 +52,17 @@ local function split(bracket_range, left, right)
|
|||||||
bracket_range:replace(code.lines)
|
bracket_range:replace(code.lines)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param bracket_range u.Range
|
---@param bracket_range Range
|
||||||
--- @param left string
|
---@param left string
|
||||||
--- @param right string
|
---@param right string
|
||||||
local function join(bracket_range, left, right)
|
local function join(bracket_range, left, right)
|
||||||
local inner_range = bracket_range:shrink(1)
|
local inner_range = Range.new(bracket_range.start:must_next(), bracket_range.stop:must_next(-1), bracket_range.mode)
|
||||||
if inner_range then
|
local newline = vim
|
||||||
local newline = vim
|
.iter(inner_range:lines())
|
||||||
.iter(inner_range:lines())
|
:map(function(l) return vim.trim(l) end)
|
||||||
:map(function(l) return vim.trim(l) end)
|
:filter(function(l) return l ~= '' end)
|
||||||
:filter(function(l) return l ~= '' end)
|
:join ' '
|
||||||
:join ' '
|
bracket_range:replace { left .. newline .. right }
|
||||||
bracket_range:replace { left .. newline .. right }
|
|
||||||
else
|
|
||||||
bracket_range:replace { left .. right }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function splitjoin()
|
local function splitjoin()
|
||||||
@@ -84,7 +80,7 @@ local function splitjoin()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function M.setup()
|
function M.setup()
|
||||||
vim.keymap.set('n', 'gS', function() vim_repeat.run_repeatable(splitjoin) end)
|
vim.keymap.set('n', 'gS', function() vim_repeat.run(splitjoin) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
|
local vim_repeat = require 'u.repeat'
|
||||||
|
local opkeymap = require 'u.opkeymap'
|
||||||
|
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 = ' )' },
|
||||||
@@ -21,17 +21,8 @@ local surrounds = {
|
|||||||
['`'] = { left = '`', right = '`' },
|
['`'] = { left = '`', right = '`' },
|
||||||
}
|
}
|
||||||
|
|
||||||
--- @type { left: string; right: string } | nil
|
---@return { left: string; right: string }|nil
|
||||||
local CACHED_BOUNDS = nil
|
|
||||||
|
|
||||||
--- @return { left: string; right: string }|nil
|
|
||||||
local function prompt_for_bounds()
|
local function prompt_for_bounds()
|
||||||
if vim_repeat.is_repeating() then
|
|
||||||
-- If we are repeating, we don't want to prompt for bounds, because
|
|
||||||
-- we want to reuse the last bounds:
|
|
||||||
return CACHED_BOUNDS
|
|
||||||
end
|
|
||||||
|
|
||||||
local cn = vim.fn.getchar()
|
local cn = vim.fn.getchar()
|
||||||
-- Check for non-printable characters:
|
-- Check for non-printable characters:
|
||||||
if type(cn) ~= 'number' or cn < 32 or cn > 126 then return end
|
if type(cn) ~= 'number' or cn < 32 or cn > 126 then return end
|
||||||
@@ -45,17 +36,15 @@ local function prompt_for_bounds()
|
|||||||
vim.keymap.del('c', '>')
|
vim.keymap.del('c', '>')
|
||||||
local endtag = '</' .. tag:sub(2):match '[^ >]*' .. '>'
|
local endtag = '</' .. tag:sub(2):match '[^ >]*' .. '>'
|
||||||
-- selene: allow(global_usage)
|
-- selene: allow(global_usage)
|
||||||
CACHED_BOUNDS = { left = tag, right = endtag }
|
return { left = tag, right = endtag }
|
||||||
return CACHED_BOUNDS
|
|
||||||
else
|
else
|
||||||
-- Default surround:
|
-- Default surround:
|
||||||
CACHED_BOUNDS = (surrounds)[c] or { left = c, right = c }
|
return (surrounds)[c] or { left = c, right = c }
|
||||||
return CACHED_BOUNDS
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param range u.Range
|
---@param range 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
|
||||||
@@ -70,7 +59,7 @@ local function do_surround(range, bounds)
|
|||||||
range:replace(left .. range:text() .. right)
|
range:replace(left .. range:text() .. right)
|
||||||
elseif range.mode == 'V' then
|
elseif range.mode == 'V' then
|
||||||
local buf = Buffer.current()
|
local buf = Buffer.current()
|
||||||
local cw = CodeWriter.from_line(range.start:line(), buf.bufnr)
|
local cw = CodeWriter.from_line(buf:line0(range.start.lnum):text(), buf.buf)
|
||||||
|
|
||||||
-- write the left bound at the current indent level:
|
-- write the left bound at the current indent level:
|
||||||
cw:write(left)
|
cw:write(left)
|
||||||
@@ -97,107 +86,97 @@ local function do_surround(range, bounds)
|
|||||||
range.start:save_to_pos '.'
|
range.start:save_to_pos '.'
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Add surround:
|
|
||||||
--- @param ty 'line' | 'char' | 'block'
|
|
||||||
function _G.MySurroundOpFunc(ty)
|
|
||||||
if ty == 'block' then
|
|
||||||
-- We won't handle block-selection:
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local range = Range.from_op_func(ty)
|
|
||||||
local hl
|
|
||||||
if not vim_repeat.is_repeating() then hl = range:highlight('IncSearch', { priority = 999 }) end
|
|
||||||
|
|
||||||
local bounds = prompt_for_bounds()
|
|
||||||
if hl then hl.clear() end
|
|
||||||
if bounds == nil then return end
|
|
||||||
|
|
||||||
do_surround(range, bounds)
|
|
||||||
end
|
|
||||||
|
|
||||||
function M.setup()
|
function M.setup()
|
||||||
require('u.repeat').setup()
|
require('u.repeat').setup()
|
||||||
|
|
||||||
-- Visual
|
-- Visual
|
||||||
vim.keymap.set('x', 'S', function()
|
vim.keymap.set('v', 'S', function()
|
||||||
|
local c = vim.fn.getcharstr()
|
||||||
local range = Range.from_vtext()
|
local range = Range.from_vtext()
|
||||||
local bounds = prompt_for_bounds()
|
local bounds = surrounds[c] or { left = c, right = c }
|
||||||
if bounds == nil then return end
|
vim_repeat.run(function()
|
||||||
|
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)
|
||||||
end, { noremap = true, silent = true })
|
end, { noremap = true, silent = true })
|
||||||
|
|
||||||
-- Change
|
-- Change
|
||||||
vim.keymap.set('n', 'cs', function()
|
vim.keymap.set('n', 'cs', function()
|
||||||
local from_cn = vim.fn.getchar() --[[@as number]]
|
local from_cn = vim.fn.getchar()
|
||||||
-- Check for non-printable characters:
|
-- Check for non-printable characters:
|
||||||
if from_cn < 32 or from_cn > 126 then return end
|
if from_cn < 32 or from_cn > 126 then return end
|
||||||
|
local from_c = vim.fn.nr2char(from_cn)
|
||||||
vim_repeat.run_repeatable(function()
|
local from = surrounds[from_c] or { left = from_c, right = from_c }
|
||||||
local from_c = vim.fn.nr2char(from_cn)
|
local function get_fresh_arange()
|
||||||
local from = surrounds[from_c] or { left = from_c, right = from_c }
|
local arange = Range.from_text_object('a' .. from_c, { user_defined = true })
|
||||||
local function get_fresh_arange()
|
if arange == nil then return nil end
|
||||||
local arange = Range.from_motion('a' .. from_c, { user_defined = true })
|
if from_c == 'q' then
|
||||||
if arange == nil then return end
|
from.left = arange.start:char()
|
||||||
if from_c == 'q' then
|
from.right = arange.stop:char()
|
||||||
from.left = arange.start:char()
|
|
||||||
from.right = arange.stop:char()
|
|
||||||
end
|
|
||||||
return arange
|
|
||||||
end
|
end
|
||||||
|
return arange
|
||||||
|
end
|
||||||
|
|
||||||
local arange = get_fresh_arange()
|
local arange = get_fresh_arange()
|
||||||
if arange == nil then return end
|
if arange == nil then return nil end
|
||||||
|
|
||||||
local hl_info1 = vim_repeat.is_repeating() and nil
|
local hl_info1 = Range.new(arange.start, arange.start, 'v'):highlight('IncSearch', { priority = 999 })
|
||||||
or Range.new(arange.start, arange.start, 'v'):highlight('IncSearch', { priority = 999 })
|
local hl_info2 = Range.new(arange.stop, arange.stop, 'v'):highlight('IncSearch', { priority = 999 })
|
||||||
local hl_info2 = vim_repeat.is_repeating() and nil
|
local hl_clear = function()
|
||||||
or Range.new(arange.stop, arange.stop, 'v'):highlight('IncSearch', { priority = 999 })
|
if hl_info1 then hl_info1.clear() end
|
||||||
local hl_clear = function()
|
if hl_info2 then hl_info2.clear() end
|
||||||
if hl_info1 then hl_info1.clear() end
|
end
|
||||||
if hl_info2 then hl_info2.clear() end
|
|
||||||
end
|
|
||||||
|
|
||||||
local to = prompt_for_bounds()
|
local to = prompt_for_bounds()
|
||||||
hl_clear()
|
hl_clear()
|
||||||
if to == nil then return end
|
if to == nil then return end
|
||||||
|
|
||||||
|
vim_repeat.run(function()
|
||||||
|
-- Re-fetch the arange, just in case this action is being repeated:
|
||||||
|
arange = get_fresh_arange()
|
||||||
|
if arange == nil then return nil end
|
||||||
|
|
||||||
if from_c == 't' then
|
if from_c == 't' then
|
||||||
-- For tags, we want to replace the inner text, not the tag:
|
-- For tags, we want to replace the inner text, not the tag:
|
||||||
local irange = Range.from_motion('i' .. from_c, { user_defined = true })
|
local irange = Range.from_text_object('i' .. from_c, { user_defined = true })
|
||||||
if arange == nil or irange == nil then return end
|
if arange == nil or irange == nil then return nil end
|
||||||
|
|
||||||
local lrange, rrange = arange:difference(irange)
|
local lrange = Range.new(arange.start, irange.start:must_next(-1))
|
||||||
if not lrange or not rrange then return end
|
local rrange = Range.new(irange.stop:must_next(1), arange.stop)
|
||||||
|
|
||||||
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 right_text = arange:sub(-1, -#from.right)
|
local last_line = arange:line0(-1).text() --[[@as string]]
|
||||||
right_text:replace(to.right)
|
local from_right_match = last_line:match(vim.pesc(from.right) .. '$')
|
||||||
|
if from_right_match then
|
||||||
|
local match_start = arange.stop:clone()
|
||||||
|
match_start.col = match_start.col - #from_right_match + 1
|
||||||
|
Range.new(match_start, arange.stop):replace(to.right)
|
||||||
|
end
|
||||||
|
|
||||||
-- replace `from.left` with `to.left`:
|
-- replace `from.left` with `to.left`:
|
||||||
local left_text = arange:sub(1, #from.left)
|
local first_line = arange:line0(0).text() --[[@as string]]
|
||||||
left_text:replace(to.left)
|
local from_left_match = first_line:match('^' .. vim.pesc(from.left))
|
||||||
|
if from_left_match then
|
||||||
|
local match_end = arange.start:clone()
|
||||||
|
match_end.col = match_end.col + #from_left_match - 1
|
||||||
|
Range.new(arange.start, match_end):replace(to.left)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end, { noremap = true, silent = true })
|
end, { noremap = true, silent = true })
|
||||||
|
|
||||||
-- Delete
|
-- Delete
|
||||||
local CACHED_DELETE_FROM = nil
|
|
||||||
vim.keymap.set('n', 'ds', function()
|
vim.keymap.set('n', 'ds', function()
|
||||||
vim_repeat.run_repeatable(function()
|
local txt_obj = vim.fn.getcharstr()
|
||||||
local txt_obj = vim_repeat.is_repeating() and CACHED_DELETE_FROM or vim.fn.getcharstr()
|
vim_repeat.run(function()
|
||||||
CACHED_DELETE_FROM = txt_obj
|
|
||||||
|
|
||||||
local buf = Buffer.current()
|
local buf = Buffer.current()
|
||||||
local irange = Range.from_motion('i' .. txt_obj)
|
local irange = Range.from_text_object('i' .. txt_obj)
|
||||||
local arange = Range.from_motion('a' .. txt_obj)
|
local arange = Range.from_text_object('a' .. txt_obj)
|
||||||
if arange == nil or irange == nil then return end
|
if arange == nil or irange == nil then return nil end
|
||||||
local starting_cursor_pos = arange.start:clone()
|
local starting_cursor_pos = arange.start:clone()
|
||||||
|
|
||||||
-- Now, replace `arange` with the content of `irange`. If `arange` was multiple lines,
|
-- Now, replace `arange` with the content of `irange`. If `arange` was multiple lines,
|
||||||
@@ -208,19 +187,28 @@ function M.setup()
|
|||||||
-- Dedenting moves the cursor, so we need to set the cursor to a consistent starting spot:
|
-- Dedenting moves the cursor, so we need to set the cursor to a consistent starting spot:
|
||||||
arange.start:save_to_pos '.'
|
arange.start:save_to_pos '.'
|
||||||
-- Dedenting also changed the inner text, so re-acquire it:
|
-- Dedenting also changed the inner text, so re-acquire it:
|
||||||
arange = Range.from_motion('a' .. txt_obj)
|
arange = Range.from_text_object('a' .. txt_obj)
|
||||||
irange = Range.from_motion('i' .. txt_obj)
|
irange = Range.from_text_object('i' .. txt_obj)
|
||||||
if arange == nil or irange == nil then return end -- should never be true
|
if arange == nil or irange == nil then return end -- should never be true
|
||||||
arange:replace(irange:lines())
|
arange:replace(irange:lines())
|
||||||
-- `arange:replace(..)` updates its own `stop` position, so we will use
|
|
||||||
-- `arange` as the final resulting range that holds the modified text
|
local final_range = Range.new(
|
||||||
|
arange.start,
|
||||||
|
Pos.new(
|
||||||
|
arange.stop.buf,
|
||||||
|
irange.start.lnum + (arange.stop.lnum + arange.start.lnum),
|
||||||
|
arange.stop.col,
|
||||||
|
arange.stop.off
|
||||||
|
),
|
||||||
|
irange.mode
|
||||||
|
)
|
||||||
|
|
||||||
-- delete last line, if it is empty:
|
-- delete last line, if it is empty:
|
||||||
local last = buf:line(arange.stop.lnum)
|
local last = buf:line0(final_range.stop.lnum)
|
||||||
if last:text():match '^%s*$' then last:replace(nil) end
|
if last:text():match '^%s*$' then last:replace(nil) end
|
||||||
|
|
||||||
-- delete first line, if it is empty:
|
-- delete first line, if it is empty:
|
||||||
local first = buf:line(arange.start.lnum)
|
local first = buf:line0(final_range.start.lnum)
|
||||||
if first:text():match '^%s*$' then first:replace(nil) end
|
if first:text():match '^%s*$' then first:replace(nil) end
|
||||||
else
|
else
|
||||||
-- trim start:
|
-- trim start:
|
||||||
@@ -232,10 +220,35 @@ function M.setup()
|
|||||||
end)
|
end)
|
||||||
end, { noremap = true, silent = true })
|
end, { noremap = true, silent = true })
|
||||||
|
|
||||||
vim.keymap.set('n', 'ys', function()
|
opkeymap('n', 'ys', function(range)
|
||||||
vim.o.operatorfunc = 'v:lua.MySurroundOpFunc'
|
local hl_info = range:highlight('IncSearch', { priority = 999 })
|
||||||
return 'g@'
|
|
||||||
end, { expr = true })
|
---@type { left: string; right: string }
|
||||||
|
local bounds
|
||||||
|
-- selene: allow(global_usage)
|
||||||
|
if _G.my_surround_bounds ~= nil then
|
||||||
|
-- This command was repeated with `.`, we don't need
|
||||||
|
-- to prompt for the bounds:
|
||||||
|
-- selene: allow(global_usage)
|
||||||
|
bounds = _G.my_surround_bounds
|
||||||
|
else
|
||||||
|
local prompted_bounds = prompt_for_bounds()
|
||||||
|
if prompted_bounds == nil and hl_info then return hl_info.clear() end
|
||||||
|
if prompted_bounds then bounds = prompted_bounds end
|
||||||
|
end
|
||||||
|
|
||||||
|
if hl_info then hl_info.clear() end
|
||||||
|
do_surround(range, bounds)
|
||||||
|
-- selene: allow(global_usage)
|
||||||
|
_G.my_surround_bounds = nil
|
||||||
|
|
||||||
|
-- return repeatable injection
|
||||||
|
return function()
|
||||||
|
-- on_repeat, we "stage" the bounds that we were originally called with:
|
||||||
|
-- selene: allow(global_usage)
|
||||||
|
_G.my_surround_bounds = bounds
|
||||||
|
end
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -1,39 +1,58 @@
|
|||||||
local Buffer = require 'u.buffer'
|
local utils = require 'u.utils'
|
||||||
local Pos = require 'u.pos'
|
local Pos = require 'u.pos'
|
||||||
local Range = require 'u.range'
|
local Range = require 'u.range'
|
||||||
local txtobj = require 'u.txtobj'
|
local Buffer = require 'u.buffer'
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
function M.setup()
|
function M.setup()
|
||||||
-- Select whole file:
|
-- Select whole file:
|
||||||
txtobj.define('ag', function() return Buffer.current():all() end)
|
utils.define_text_object('ag', function() return Buffer.current():all() end)
|
||||||
|
|
||||||
-- Select current line:
|
-- Select current line:
|
||||||
txtobj.define('a.', function() return Buffer.current():line(Pos.from_pos('.').lnum) end)
|
utils.define_text_object('a.', function()
|
||||||
|
local lnum = Pos.from_pos('.').lnum
|
||||||
|
return Buffer.current():line0(lnum)
|
||||||
|
end)
|
||||||
|
|
||||||
-- Select the nearest quote:
|
-- Select the nearest quote:
|
||||||
txtobj.define('aq', function() return Range.find_nearest_quotes() end)
|
utils.define_text_object('aq', function() return Range.find_nearest_quotes() end)
|
||||||
txtobj.define('iq', function()
|
utils.define_text_object('iq', function()
|
||||||
local range = Range.find_nearest_quotes()
|
local range = Range.find_nearest_quotes()
|
||||||
if range == nil then return end
|
if range == nil then return end
|
||||||
return range:shrink(1)
|
return range:shrink(1)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
---Selects the next quote object (searches forward)
|
---Selects the next quote object (searches forward)
|
||||||
--- @param q string
|
---@param q string
|
||||||
local function define_quote_obj(q)
|
local function define_quote_obj(q)
|
||||||
local function select_around() return Range.from_motion('a' .. q) end
|
local function select_around()
|
||||||
|
-- Operator mappings are effectively running in visual mode, the way
|
||||||
|
-- `define_text_object` is implemented, so feed the keys `a${q}` to vim
|
||||||
|
-- to select the appropriate text-object
|
||||||
|
vim.cmd { cmd = 'normal', args = { 'a' .. q }, bang = true }
|
||||||
|
|
||||||
txtobj.define('a' .. q, function() return select_around() end)
|
-- Now check on the visually selected text:
|
||||||
txtobj.define('i' .. q, function()
|
local range = Range.from_vtext()
|
||||||
local range = select_around()
|
if range:is_empty() then return range.start end
|
||||||
if range == nil or range:is_empty() then return range end
|
range.start = range.start:find_next(1, q) or range.start
|
||||||
|
range.stop = range.stop:find_next(-1, q) or range.stop
|
||||||
|
return range
|
||||||
|
end
|
||||||
|
|
||||||
local start_next = range.start:next(1) or range.start
|
utils.define_text_object('a' .. q, function() return select_around() end)
|
||||||
local stop_prev = range.stop:next(-1)
|
utils.define_text_object('i' .. q, function()
|
||||||
if start_next > stop_prev then return Range.new(start_next) end
|
local range_or_pos = select_around()
|
||||||
return range:shrink(1) or range
|
if Range.is(range_or_pos) then
|
||||||
|
local start_next = range_or_pos.start:next(1)
|
||||||
|
local stop_prev = range_or_pos.stop:next(-1)
|
||||||
|
if start_next > stop_prev then return start_next end
|
||||||
|
|
||||||
|
local range = range_or_pos:shrink(1)
|
||||||
|
return range
|
||||||
|
else
|
||||||
|
return range_or_pos
|
||||||
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
define_quote_obj [["]]
|
define_quote_obj [["]]
|
||||||
@@ -41,26 +60,36 @@ function M.setup()
|
|||||||
define_quote_obj [[`]]
|
define_quote_obj [[`]]
|
||||||
|
|
||||||
---Selects the "last" quote object (searches backward)
|
---Selects the "last" quote object (searches backward)
|
||||||
--- @param q string
|
---@param q string
|
||||||
local function define_last_quote_obj(q)
|
local function define_last_quote_obj(q)
|
||||||
local function select_around()
|
local function select_around()
|
||||||
local curr = Pos.from_pos('.'):find_next(-1, q)
|
local curr = Pos.from_pos('.'):find_next(-1, q)
|
||||||
if not curr then return end
|
if not curr then return end
|
||||||
-- Reset visual selection to current context:
|
-- Reset visual selection to current context:
|
||||||
curr:save_to_pos '.'
|
Range.new(curr, curr):set_visual_selection()
|
||||||
return Range.from_motion('a' .. q)
|
vim.cmd.normal('a' .. q)
|
||||||
|
local range = Range.from_vtext()
|
||||||
|
if range:is_empty() then return range.start end
|
||||||
|
range.start = range.start:find_next(1, q) or range.start
|
||||||
|
range.stop = range.stop:find_next(-1, q) or range.stop
|
||||||
|
return range
|
||||||
end
|
end
|
||||||
|
|
||||||
txtobj.define('al' .. q, function() return select_around() end)
|
utils.define_text_object('al' .. q, function() return select_around() end)
|
||||||
txtobj.define('il' .. q, function()
|
utils.define_text_object('il' .. q, function()
|
||||||
local range = select_around()
|
local range_or_pos = select_around()
|
||||||
if range == nil or range:is_empty() then return range end
|
if range_or_pos == nil then return end
|
||||||
|
|
||||||
local start_next = range.start:next(1) or range.start
|
if Range.is(range_or_pos) then
|
||||||
local stop_prev = range.stop:next(-1)
|
local start_next = range_or_pos.start:next(1)
|
||||||
if start_next > stop_prev then return Range.new(start_next) end
|
local stop_prev = range_or_pos.stop:next(-1)
|
||||||
|
if start_next > stop_prev then return start_next end
|
||||||
|
|
||||||
return range:shrink(1) or range
|
local range = range_or_pos:shrink(1)
|
||||||
|
return range
|
||||||
|
else
|
||||||
|
return range_or_pos
|
||||||
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
define_last_quote_obj [["]]
|
define_last_quote_obj [["]]
|
||||||
@@ -82,8 +111,8 @@ function M.setup()
|
|||||||
local keybinds = { ... }
|
local keybinds = { ... }
|
||||||
table.insert(keybinds, b)
|
table.insert(keybinds, b)
|
||||||
for _, k in ipairs(keybinds) do
|
for _, k in ipairs(keybinds) do
|
||||||
txtobj.define('al' .. k, function() return select_around() end)
|
utils.define_text_object('al' .. k, function() return select_around() end)
|
||||||
txtobj.define('il' .. k, function()
|
utils.define_text_object('il' .. k, function()
|
||||||
local range = select_around()
|
local range = select_around()
|
||||||
return range and range:shrink(1)
|
return range and range:shrink(1)
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
|
|
||||||
"diagnostics.globals": ["assert", "vim"],
|
|
||||||
"runtime.version": "LuaJIT"
|
|
||||||
}
|
|
||||||
149
lua/u/buffer.lua
149
lua/u/buffer.lua
@@ -1,131 +1,86 @@
|
|||||||
local Range = require 'u.range'
|
local Range = require 'u.range'
|
||||||
local Renderer = require('u.renderer').Renderer
|
local Renderer = require 'u.renderer'.Renderer
|
||||||
|
|
||||||
--- @class u.Buffer
|
---@class Buffer
|
||||||
--- @field bufnr integer
|
---@field buf number
|
||||||
--- @field b vim.var_accessor
|
---@field private renderer Renderer
|
||||||
--- @field bo vim.bo
|
|
||||||
--- @field renderer u.renderer.Renderer
|
|
||||||
local Buffer = {}
|
local Buffer = {}
|
||||||
Buffer.__index = Buffer
|
|
||||||
|
|
||||||
--- @param bufnr? number
|
---@param buf? number
|
||||||
--- @return u.Buffer
|
---@return Buffer
|
||||||
function Buffer.from_nr(bufnr)
|
function Buffer.from_nr(buf)
|
||||||
if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
|
if buf == nil or buf == 0 then buf = vim.api.nvim_get_current_buf() end
|
||||||
local renderer = Renderer.new(bufnr)
|
|
||||||
|
local renderer = Renderer.new(buf)
|
||||||
return setmetatable({
|
return setmetatable({
|
||||||
bufnr = bufnr,
|
buf = buf,
|
||||||
b = vim.b[bufnr],
|
|
||||||
bo = vim.bo[bufnr],
|
|
||||||
renderer = renderer,
|
renderer = renderer,
|
||||||
}, Buffer)
|
}, { __index = Buffer })
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @return u.Buffer
|
---@return Buffer
|
||||||
function Buffer.current() return Buffer.from_nr(0) end
|
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 Buffer
|
||||||
function Buffer.create(listed, scratch)
|
function Buffer.create(listed, scratch) return Buffer.from_nr(vim.api.nvim_create_buf(listed, scratch)) end
|
||||||
return Buffer.from_nr(vim.api.nvim_create_buf(listed, scratch))
|
|
||||||
end
|
|
||||||
|
|
||||||
function Buffer:set_tmp_options()
|
function Buffer:set_tmp_options()
|
||||||
self.bo.bufhidden = 'delete'
|
self:set_option('bufhidden', 'delete')
|
||||||
self.bo.buflisted = false
|
self:set_option('buflisted', false)
|
||||||
self.bo.buftype = 'nowrite'
|
self:set_option('buftype', 'nowrite')
|
||||||
end
|
end
|
||||||
|
|
||||||
function Buffer:line_count() return vim.api.nvim_buf_line_count(self.bufnr) end
|
---@param nm string
|
||||||
|
function Buffer:get_option(nm) return vim.api.nvim_get_option_value(nm, { buf = self.buf }) end
|
||||||
|
|
||||||
function Buffer:all() return Range.from_buf_text(self.bufnr) end
|
---@param nm string
|
||||||
|
function Buffer:set_option(nm, val) return vim.api.nvim_set_option_value(nm, val, { buf = self.buf }) end
|
||||||
|
|
||||||
function Buffer:is_empty() return self:line_count() == 1 and self:line(1):text() == '' end
|
---@param nm string
|
||||||
|
function Buffer:get_var(nm) return vim.api.nvim_buf_get_var(self.buf, nm) end
|
||||||
|
|
||||||
--- @param line string
|
---@param nm string
|
||||||
|
function Buffer:set_var(nm, val) return vim.api.nvim_buf_set_var(self.buf, nm, val) end
|
||||||
|
|
||||||
|
function Buffer:line_count() return vim.api.nvim_buf_line_count(self.buf) end
|
||||||
|
|
||||||
|
function Buffer:all() return Range.from_buf_text(self.buf) end
|
||||||
|
|
||||||
|
function Buffer:is_empty() return self:line_count() == 1 and self:line0(0):text() == '' end
|
||||||
|
|
||||||
|
---@param line string
|
||||||
function Buffer:append_line(line)
|
function Buffer:append_line(line)
|
||||||
local start = -1
|
local start = -1
|
||||||
if self:is_empty() then start = -2 end
|
if self:is_empty() then start = -2 end
|
||||||
vim.api.nvim_buf_set_lines(self.bufnr, start, -1, false, { line })
|
vim.api.nvim_buf_set_lines(self.buf, start, -1, false, { line })
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param num number 1-based line index
|
---@param num number 0-based line index
|
||||||
function Buffer:line(num)
|
function Buffer:line0(num)
|
||||||
if num < 0 then num = self:line_count() + num + 1 end
|
if num < 0 then return self:line0(self:line_count() + num) end
|
||||||
return Range.from_line(self.bufnr, num)
|
return Range.from_line(self.buf, num)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param start number 1-based line index
|
---@param start number 0-based line index
|
||||||
--- @param stop number 1-based line index
|
---@param stop number 0-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.buf, start, stop) end
|
||||||
|
|
||||||
--- @param motion string
|
---@param txt_obj string
|
||||||
--- @param opts? { contains_cursor?: boolean; pos?: u.Pos }
|
---@param opts? { contains_cursor?: boolean; pos?: Pos }
|
||||||
function Buffer:motion(motion, opts)
|
function Buffer:text_object(txt_obj, opts)
|
||||||
opts = vim.tbl_extend('force', opts or {}, { bufnr = self.bufnr })
|
opts = vim.tbl_extend('force', opts or {}, { buf = self.buf })
|
||||||
return Range.from_motion(motion, opts)
|
return Range.from_text_object(txt_obj, opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param event vim.api.keyset.events|vim.api.keyset.events[]
|
--- @param event string|string[]
|
||||||
--- @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.buf }))
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param fn function
|
--- @param tree Tree
|
||||||
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
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
local Buffer = require 'u.buffer'
|
local Buffer = require 'u.buffer'
|
||||||
|
|
||||||
--- @class u.CodeWriter
|
---@class CodeWriter
|
||||||
--- @field lines string[]
|
---@field lines string[]
|
||||||
--- @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
|
||||||
--- @return u.CodeWriter
|
---@return CodeWriter
|
||||||
function CodeWriter.new(indent_level, indent_str)
|
function CodeWriter.new(indent_level, indent_str)
|
||||||
if indent_level == nil then indent_level = 0 end
|
if indent_level == nil then indent_level = 0 end
|
||||||
if indent_str == nil then indent_str = ' ' end
|
if indent_str == nil then indent_str = ' ' end
|
||||||
@@ -19,27 +18,26 @@ 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, CodeWriter)
|
setmetatable(cw, { __index = CodeWriter })
|
||||||
return cw
|
return cw
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param p u.Pos
|
---@param p Pos
|
||||||
function CodeWriter.from_pos(p)
|
function CodeWriter.from_pos(p)
|
||||||
local line = Buffer.from_nr(p.bufnr):line(p.lnum):text()
|
local line = Buffer.from_nr(p.buf):line0(p.lnum):text()
|
||||||
return CodeWriter.from_line(line, p.bufnr)
|
return CodeWriter.from_line(line, p.buf)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param line string
|
---@param line string
|
||||||
--- @param bufnr? number
|
---@param buf? number
|
||||||
function CodeWriter.from_line(line, bufnr)
|
function CodeWriter.from_line(line, buf)
|
||||||
if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
|
if buf == nil then buf = 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 = buf })
|
||||||
local shiftwidth = vim.api.nvim_get_option_value('shiftwidth', { buf = bufnr })
|
local shiftwidth = vim.api.nvim_get_option_value('shiftwidth', { buf = buf })
|
||||||
|
|
||||||
--- @type number
|
local indent_level = 0
|
||||||
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
|
||||||
@@ -54,16 +52,16 @@ function CodeWriter.from_line(line, bufnr)
|
|||||||
return CodeWriter.new(indent_level, indent_str)
|
return CodeWriter.new(indent_level, indent_str)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param line string
|
---@param line string
|
||||||
function CodeWriter:write_raw(line)
|
function CodeWriter:write_raw(line)
|
||||||
if line:find '\n' then error 'line contains newline character' end
|
if line:find '\n' then error 'line contains newline character' end
|
||||||
table.insert(self.lines, line)
|
table.insert(self.lines, line)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param line string
|
---@param line string
|
||||||
function CodeWriter:write(line) self:write_raw(self.indent_str:rep(self.indent_level) .. line) end
|
function CodeWriter:write(line) self:write_raw(self.indent_str:rep(self.indent_level) .. line) end
|
||||||
|
|
||||||
--- @param f? fun(cw: u.CodeWriter):any
|
---@param f? fun(cw: CodeWriter):any
|
||||||
function CodeWriter:indent(f)
|
function CodeWriter:indent(f)
|
||||||
local cw = {
|
local cw = {
|
||||||
lines = self.lines,
|
lines = self.lines,
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
local Pos = require 'u.pos'
|
|
||||||
|
|
||||||
---@class u.Extmark
|
|
||||||
---@field bufnr integer
|
|
||||||
---@field id integer
|
|
||||||
---@field nsid integer
|
|
||||||
local Extmark = {}
|
|
||||||
Extmark.__index = Extmark
|
|
||||||
|
|
||||||
--- @param bufnr integer
|
|
||||||
--- @param nsid integer
|
|
||||||
--- @param id integer
|
|
||||||
function Extmark.new(bufnr, nsid, id)
|
|
||||||
return setmetatable({
|
|
||||||
bufnr = bufnr,
|
|
||||||
nsid = nsid,
|
|
||||||
id = id,
|
|
||||||
}, Extmark)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- @param range u.Range
|
|
||||||
--- @param nsid integer
|
|
||||||
function Extmark.from_range(range, nsid)
|
|
||||||
local r = range:to_charwise()
|
|
||||||
local stop = r.stop or r.start
|
|
||||||
local end_row = stop.lnum - 1
|
|
||||||
local end_col = stop.col
|
|
||||||
if range.mode == 'V' then
|
|
||||||
end_row = end_row + 1
|
|
||||||
end_col = 0
|
|
||||||
end
|
|
||||||
local id = vim.api.nvim_buf_set_extmark(r.start.bufnr, nsid, r.start.lnum - 1, r.start.col - 1, {
|
|
||||||
right_gravity = false,
|
|
||||||
end_right_gravity = true,
|
|
||||||
end_row = end_row,
|
|
||||||
end_col = end_col,
|
|
||||||
})
|
|
||||||
return Extmark.new(r.start.bufnr, nsid, id)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Extmark:range()
|
|
||||||
local Range = require 'u.range'
|
|
||||||
|
|
||||||
local raw_extmark =
|
|
||||||
vim.api.nvim_buf_get_extmark_by_id(self.bufnr, self.nsid, self.id, { details = true })
|
|
||||||
local start_row0, start_col0, details = unpack(raw_extmark)
|
|
||||||
|
|
||||||
--- @type u.Pos
|
|
||||||
local start = Pos.from00(self.bufnr, start_row0, start_col0)
|
|
||||||
--- @type u.Pos?
|
|
||||||
local stop = details
|
|
||||||
and details.end_row
|
|
||||||
and details.end_col
|
|
||||||
and Pos.from01(self.bufnr, details.end_row, details.end_col)
|
|
||||||
|
|
||||||
local n_buf_lines = vim.api.nvim_buf_line_count(self.bufnr)
|
|
||||||
if stop and stop.lnum > n_buf_lines then
|
|
||||||
stop.lnum = n_buf_lines
|
|
||||||
stop = stop:eol()
|
|
||||||
end
|
|
||||||
if stop and stop.col == 0 then
|
|
||||||
stop.col = 1
|
|
||||||
stop = stop:next(-1)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Range.new(start, stop, 'v')
|
|
||||||
end
|
|
||||||
|
|
||||||
function Extmark:delete() vim.api.nvim_buf_del_extmark(self.bufnr, self.nsid, self.id) end
|
|
||||||
|
|
||||||
return Extmark
|
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
local LOG_ROOT = vim.fs.joinpath(vim.fn.stdpath 'cache', 'u.log')
|
|
||||||
|
|
||||||
--- @params name string
|
--- @params name string
|
||||||
function M.file_for_name(name) return vim.fs.joinpath(LOG_ROOT, name .. '.log.jsonl') end
|
function M.file_for_name(name) return vim.fs.joinpath(vim.fn.stdpath 'cache', 'my.log', name .. '.log.jsonl') end
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- Logger class
|
-- Logger class
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
--- @class u.Logger
|
--- @class Logger
|
||||||
--- @field name string
|
--- @field name string
|
||||||
|
--- @field private fd number
|
||||||
local Logger = {}
|
local Logger = {}
|
||||||
Logger.__index = Logger
|
Logger.__index = Logger
|
||||||
M.Logger = Logger
|
M.Logger = Logger
|
||||||
@@ -19,7 +18,10 @@ M.Logger = Logger
|
|||||||
function Logger.new(name)
|
function Logger.new(name)
|
||||||
local file_path = M.file_for_name(name)
|
local file_path = M.file_for_name(name)
|
||||||
vim.fn.mkdir(vim.fs.dirname(file_path), 'p')
|
vim.fn.mkdir(vim.fs.dirname(file_path), 'p')
|
||||||
local self = setmetatable({ name = name }, Logger)
|
local self = setmetatable({
|
||||||
|
name = name,
|
||||||
|
fd = (vim.uv or vim.loop).fs_open(file_path, 'a', tonumber('644', 8)),
|
||||||
|
}, Logger)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -28,12 +30,7 @@ end
|
|||||||
function Logger:write(level, ...)
|
function Logger:write(level, ...)
|
||||||
local data = { ... }
|
local data = { ... }
|
||||||
if #data == 1 then data = data[1] end
|
if #data == 1 then data = data[1] end
|
||||||
local f = assert(io.open(M.file_for_name(self.name), 'a'), 'could not open file')
|
(vim.uv or vim.loop).fs_write(self.fd, vim.json.encode { ts = os.date(), level = level, data = data } .. '\n')
|
||||||
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
|
||||||
@@ -52,22 +49,8 @@ function M.setup()
|
|||||||
local log_file_path = M.file_for_name(args.fargs[1])
|
local log_file_path = M.file_for_name(args.fargs[1])
|
||||||
vim.fn.mkdir(vim.fs.dirname(log_file_path), 'p')
|
vim.fn.mkdir(vim.fs.dirname(log_file_path), 'p')
|
||||||
vim.system({ 'touch', log_file_path }):wait()
|
vim.system({ 'touch', log_file_path }):wait()
|
||||||
|
vim.cmd.Term('tail -f "' .. log_file_path .. '"')
|
||||||
vim.cmd.new()
|
|
||||||
|
|
||||||
local winnr = vim.api.nvim_get_current_win()
|
|
||||||
vim.wo[winnr][0].number = false
|
|
||||||
vim.wo[winnr][0].relativenumber = false
|
|
||||||
|
|
||||||
vim.cmd.terminal('tail -f "' .. log_file_path .. '"')
|
|
||||||
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
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
local Range = require 'u.range'
|
local Range = require 'u.range'
|
||||||
|
local vim_repeat = require 'u.repeat'
|
||||||
|
|
||||||
--- @type fun(range: u.Range): nil|(fun():any)
|
---@type fun(range: Range): nil|(fun():any)
|
||||||
local __U__OpKeymapOpFunc_rhs = nil
|
local __U__OpKeymapOpFunc_rhs = nil
|
||||||
|
|
||||||
--- This is the global utility function used for operatorfunc
|
--- This is the global utility function used for operatorfunc
|
||||||
--- in opkeymap
|
--- in opkeymap
|
||||||
--- @type nil|fun(range: u.Range): fun():any|nil
|
---@type nil|fun(range: Range): fun():any|nil
|
||||||
--- @param ty 'line'|'char'|'block'
|
---@param ty 'line'|'char'|'block'
|
||||||
-- selene: allow(unused_variable)
|
-- selene: allow(unused_variable)
|
||||||
function _G.__U__OpKeymapOpFunc(ty)
|
function __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)
|
local repeat_inject = __U__OpKeymapOpFunc_rhs(range)
|
||||||
|
|
||||||
|
vim_repeat.set(function()
|
||||||
|
vim.o.operatorfunc = 'v:lua.__U__OpKeymapOpFunc'
|
||||||
|
if repeat_inject ~= nil and type(repeat_inject) == 'function' then repeat_inject() end
|
||||||
|
vim_repeat.native_repeat()
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -21,17 +28,12 @@ end
|
|||||||
--- g@: tells vim to way for a motion, and then call operatorfunc.
|
--- g@: tells vim to way for a motion, and then call operatorfunc.
|
||||||
--- 2. The operatorfunc is set to a lua function that computes the range being operated over, that
|
--- 2. The operatorfunc is set to a lua function that computes the range being operated over, that
|
||||||
--- then calls the original passed callback with said range.
|
--- then calls the original passed callback with said range.
|
||||||
--- @param mode string|string[]
|
---@param mode string|string[]
|
||||||
--- @param lhs string
|
---@param lhs string
|
||||||
--- @param rhs fun(range: u.Range): nil
|
---@param rhs fun(range: Range): nil|(fun():any) This function may return another function, which is called whenever the operator is repeated
|
||||||
--- @diagnostic disable-next-line: undefined-doc-name
|
---@param opts? vim.keymap.set.Opts
|
||||||
--- @param opts? vim.keymap.set.Opts
|
|
||||||
local function opkeymap(mode, lhs, rhs, opts)
|
local function opkeymap(mode, lhs, rhs, opts)
|
||||||
vim.keymap.set(mode, lhs, function()
|
vim.keymap.set(mode, lhs, function()
|
||||||
-- We don't need to wrap the operation in a repeat, because expr mappings are
|
|
||||||
-- repeated seamlessly by Vim anyway. In addition, the u.repeat:`.` mapping will
|
|
||||||
-- set IS_REPEATING to true, so that callbacks can check if they should used cached
|
|
||||||
-- values.
|
|
||||||
__U__OpKeymapOpFunc_rhs = rhs
|
__U__OpKeymapOpFunc_rhs = rhs
|
||||||
vim.o.operatorfunc = 'v:lua.__U__OpKeymapOpFunc'
|
vim.o.operatorfunc = 'v:lua.__U__OpKeymapOpFunc'
|
||||||
return 'g@'
|
return 'g@'
|
||||||
|
|||||||
256
lua/u/pos.lua
256
lua/u/pos.lua
@@ -1,203 +1,162 @@
|
|||||||
local MAX_COL = vim.v.maxcol
|
local MAX_COL = vim.v.maxcol
|
||||||
|
|
||||||
--- @param bufnr number
|
---@param buf number
|
||||||
--- @param lnum number 1-based
|
---@param lnum number
|
||||||
local function line_text(bufnr, lnum)
|
local function line_text(buf, lnum) return vim.api.nvim_buf_get_lines(buf, lnum, lnum + 1, false)[1] end
|
||||||
return vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, false)[1]
|
|
||||||
end
|
|
||||||
|
|
||||||
--- @class u.Pos
|
---@class Pos
|
||||||
--- @field bufnr integer buffer number
|
---@field buf number buffer number
|
||||||
--- @field lnum integer 1-based line index
|
---@field lnum number 1-based line index
|
||||||
--- @field col integer 1-based column index
|
---@field col number 1-based column index
|
||||||
--- @field off number
|
---@field off number
|
||||||
local Pos = {}
|
local Pos = {}
|
||||||
Pos.__index = Pos
|
|
||||||
Pos.MAX_COL = MAX_COL
|
Pos.MAX_COL = MAX_COL
|
||||||
|
|
||||||
function Pos.__tostring(self)
|
---@param buf? number
|
||||||
if self.off ~= 0 then
|
---@param lnum number
|
||||||
return string.format('Pos(%d:%d){bufnr=%d, off=%d}', self.lnum, self.col, self.bufnr, self.off)
|
---@param col number
|
||||||
else
|
---@param off? number
|
||||||
return string.format('Pos(%d:%d){bufnr=%d}', self.lnum, self.col, self.bufnr)
|
---@return Pos
|
||||||
end
|
function Pos.new(buf, lnum, col, off)
|
||||||
end
|
if buf == nil or buf == 0 then buf = vim.api.nvim_get_current_buf() end
|
||||||
|
|
||||||
--- @param bufnr? number
|
|
||||||
--- @param lnum number 1-based
|
|
||||||
--- @param col number 1-based
|
|
||||||
--- @param off? number
|
|
||||||
--- @return u.Pos
|
|
||||||
function Pos.new(bufnr, lnum, col, off)
|
|
||||||
if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
|
|
||||||
if off == nil then off = 0 end
|
if off == nil then off = 0 end
|
||||||
--- @type u.Pos
|
local pos = {
|
||||||
return setmetatable({
|
buf = buf,
|
||||||
bufnr = bufnr,
|
|
||||||
lnum = lnum,
|
lnum = lnum,
|
||||||
col = col,
|
col = col,
|
||||||
off = off,
|
off = off,
|
||||||
}, Pos)
|
}
|
||||||
end
|
|
||||||
|
|
||||||
--- @param bufnr? number
|
local function str()
|
||||||
--- @param lnum0 number 1-based
|
if pos.off ~= 0 then
|
||||||
--- @param col0 number 1-based
|
return string.format('Pos(%d:%d){buf=%d, off=%d}', pos.lnum, pos.col, pos.buf, pos.off)
|
||||||
--- @param off? number
|
else
|
||||||
function Pos.from00(bufnr, lnum0, col0, off) return Pos.new(bufnr, lnum0 + 1, col0 + 1, off) end
|
return string.format('Pos(%d:%d){buf=%d}', pos.lnum, pos.col, pos.buf)
|
||||||
|
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.__lt(a, b) return a.lnum < b.lnum or (a.lnum == b.lnum and a.col < b.col) end
|
|
||||||
function Pos.__le(a, b) return a < b or a == b end
|
|
||||||
function Pos.__eq(a, b)
|
|
||||||
return getmetatable(a) == Pos
|
|
||||||
and getmetatable(b) == Pos
|
|
||||||
and a.bufnr == b.bufnr
|
|
||||||
and a.lnum == b.lnum
|
|
||||||
and a.col == b.col
|
|
||||||
end
|
|
||||||
function Pos.__add(x, y)
|
|
||||||
if type(x) == 'number' then
|
|
||||||
x, y = y, x
|
|
||||||
end
|
end
|
||||||
if getmetatable(x) ~= Pos or type(y) ~= 'number' then return nil end
|
setmetatable(pos, {
|
||||||
return x:next(y)
|
__index = Pos,
|
||||||
end
|
__tostring = str,
|
||||||
function Pos.__sub(x, y)
|
__lt = Pos.__lt,
|
||||||
if type(x) == 'number' then
|
__le = Pos.__le,
|
||||||
x, y = y, x
|
__eq = Pos.__eq,
|
||||||
end
|
})
|
||||||
if getmetatable(x) ~= Pos or type(y) ~= 'number' then return nil end
|
|
||||||
return x:next(-y)
|
|
||||||
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
|
return pos
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param name string
|
function Pos.is(x)
|
||||||
--- @return u.Pos
|
local mt = getmetatable(x)
|
||||||
function Pos.from_pos(name)
|
return mt and mt.__index == Pos
|
||||||
local p = vim.fn.getpos(name)
|
|
||||||
return Pos.new(p[1], p[2], p[3], p[4])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Pos:is_invalid() return self.lnum == 0 and self.col == 0 and self.off == 0 end
|
function Pos.__lt(a, b) return a.lnum < b.lnum or (a.lnum == b.lnum and a.col < b.col) end
|
||||||
|
function Pos.__le(a, b) return a < b or a == b end
|
||||||
|
function Pos.__eq(a, b) return a.lnum == b.lnum and a.col == b.col end
|
||||||
|
|
||||||
function Pos:clone() return Pos.new(self.bufnr, self.lnum, self.col, self.off) end
|
---@param name string
|
||||||
|
---@return Pos
|
||||||
|
function Pos.from_pos(name)
|
||||||
|
local p = vim.fn.getpos(name)
|
||||||
|
local col = p[3]
|
||||||
|
if col ~= MAX_COL then col = col - 1 end
|
||||||
|
return Pos.new(p[1], p[2] - 1, col, p[4])
|
||||||
|
end
|
||||||
|
|
||||||
--- @return boolean
|
function Pos:clone() return Pos.new(self.buf, self.lnum, self.col, self.off) end
|
||||||
|
|
||||||
|
---@return boolean
|
||||||
function Pos:is_col_max() return self.col == MAX_COL end
|
function Pos:is_col_max() return self.col == MAX_COL end
|
||||||
|
|
||||||
|
---@return number[]
|
||||||
|
function Pos:as_vim() return { self.buf, self.lnum, self.col, self.off } end
|
||||||
|
|
||||||
--- Normalize the position to a real position (take into account vim.v.maxcol).
|
--- Normalize the position to a real position (take into account vim.v.maxcol).
|
||||||
function Pos:as_real()
|
function Pos:as_real()
|
||||||
local maxlen = #line_text(self.bufnr, self.lnum)
|
|
||||||
local col = self.col
|
local col = self.col
|
||||||
if col > maxlen then
|
if self:is_col_max() then
|
||||||
-- We could use utilities in this file to get the given line, but
|
-- We could use utilities in this file to get the given line, but
|
||||||
-- since this is a low-level function, we are going to optimize and
|
-- since this is a low-level function, we are going to optimize and
|
||||||
-- use the API directly:
|
-- use the API directly:
|
||||||
col = maxlen
|
col = #line_text(self.buf, self.lnum) - 1
|
||||||
end
|
end
|
||||||
return Pos.new(self.bufnr, self.lnum, col, self.off)
|
return Pos.new(self.buf, self.lnum, col, self.off)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Pos:as_vim() return { self.bufnr, self.lnum, self.col, self.off } end
|
---@param pos string
|
||||||
|
function Pos:save_to_pos(pos)
|
||||||
|
if pos == '.' then
|
||||||
|
vim.api.nvim_win_set_cursor(0, { self.lnum + 1, self.col })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
function Pos:eol() return Pos.from_eol(self.bufnr, self.lnum) end
|
local p = self:as_real()
|
||||||
|
vim.fn.setpos(pos, { p.buf, p.lnum + 1, p.col + 1, p.off })
|
||||||
--- @param pos string
|
|
||||||
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
|
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()
|
||||||
vim.api.nvim_buf_set_mark(p.bufnr, mark, p.lnum, p.col - 1, {})
|
vim.api.nvim_buf_set_mark(p.buf, mark, p.lnum + 1, p.col, {})
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @return string
|
---@return string
|
||||||
function Pos:char()
|
function Pos:char()
|
||||||
local line = line_text(self.bufnr, self.lnum)
|
local line = line_text(self.buf, self.lnum)
|
||||||
if line == nil then return '' end
|
if line == nil then return '' end
|
||||||
return line:sub(self.col, self.col)
|
return line:sub(self.col + 1, self.col + 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Pos:line() return line_text(self.bufnr, self.lnum) end
|
---@param dir? -1|1
|
||||||
|
---@param must? boolean
|
||||||
--- @param dir? -1|1
|
---@return Pos|nil
|
||||||
--- @param must? boolean
|
|
||||||
--- @return u.Pos|nil
|
|
||||||
function Pos:next(dir, must)
|
function Pos:next(dir, must)
|
||||||
if must == nil then must = false end
|
if must == nil then must = false end
|
||||||
|
|
||||||
if dir == nil or dir == 1 then
|
if dir == nil or dir == 1 then
|
||||||
-- Next:
|
-- Next:
|
||||||
local num_lines = vim.api.nvim_buf_line_count(self.bufnr)
|
local num_lines = vim.api.nvim_buf_line_count(self.buf)
|
||||||
local last_line = line_text(self.bufnr, num_lines)
|
local last_line = line_text(self.buf, num_lines - 1) -- buf:line0(-1)
|
||||||
if self.lnum == num_lines and self.col == #last_line then
|
if self.lnum == num_lines - 1 and self.col == (#last_line - 1) then
|
||||||
if must then error 'error in Pos:next(): Pos:next() returned nil' end
|
if must then error 'error in Pos:next(): Pos:next() returned nil' end
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local col = self.col + 1
|
local col = self.col + 1
|
||||||
local line = self.lnum
|
local line = self.lnum
|
||||||
local line_max_col = #line_text(self.bufnr, self.lnum)
|
local line_max_col = #line_text(self.buf, self.lnum) - 1
|
||||||
if col > line_max_col then
|
if col > line_max_col then
|
||||||
col = 1
|
col = 0
|
||||||
line = line + 1
|
line = line + 1
|
||||||
end
|
end
|
||||||
return Pos.new(self.bufnr, line, col, self.off)
|
return Pos.new(self.buf, line, col, self.off)
|
||||||
else
|
else
|
||||||
-- Previous:
|
-- Previous:
|
||||||
if self.col == 1 and self.lnum == 1 then
|
if self.col == 0 and self.lnum == 0 then
|
||||||
if must then error 'error in Pos:next(): Pos:next() returned nil' end
|
if must then error 'error in Pos:next(): Pos:next() returned nil' end
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local col = self.col - 1
|
local col = self.col - 1
|
||||||
local line = self.lnum
|
local line = self.lnum
|
||||||
local prev_line_max_col = #(line_text(self.bufnr, self.lnum - 1) or '')
|
local prev_line_max_col = #(line_text(self.buf, self.lnum - 1) or '') - 1
|
||||||
if col < 1 then
|
if col < 0 then
|
||||||
col = math.max(prev_line_max_col, 1)
|
col = math.max(prev_line_max_col, 0)
|
||||||
line = line - 1
|
line = line - 1
|
||||||
end
|
end
|
||||||
return Pos.new(self.bufnr, line, col, self.off)
|
return Pos.new(self.buf, line, col, self.off)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param dir? -1|1
|
---@param dir? -1|1
|
||||||
function Pos:must_next(dir)
|
function Pos:must_next(dir)
|
||||||
local next = self:next(dir, true)
|
local next = self:next(dir, true)
|
||||||
if next == nil then error 'unreachable' end
|
if next == nil then error 'unreachable' end
|
||||||
return next
|
return next
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param dir -1|1
|
---@param dir -1|1
|
||||||
--- @param predicate fun(p: u.Pos): boolean
|
---@param predicate fun(p: Pos): boolean
|
||||||
--- @param test_current? boolean
|
---@param test_current? boolean
|
||||||
function Pos:next_while(dir, predicate, test_current)
|
function Pos:next_while(dir, predicate, test_current)
|
||||||
if test_current and not predicate(self) then return end
|
if test_current and not predicate(self) then return end
|
||||||
local curr = self
|
local curr = self
|
||||||
@@ -209,15 +168,15 @@ function Pos:next_while(dir, predicate, test_current)
|
|||||||
return curr
|
return curr
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param dir -1|1
|
---@param dir -1|1
|
||||||
--- @param predicate string|fun(p: u.Pos): boolean
|
---@param predicate string|fun(p: Pos): boolean
|
||||||
function Pos:find_next(dir, predicate)
|
function Pos:find_next(dir, predicate)
|
||||||
if type(predicate) == 'string' then
|
if type(predicate) == 'string' then
|
||||||
local s = predicate
|
local s = predicate
|
||||||
predicate = function(p) return s == p:char() end
|
predicate = function(p) return s == p:char() end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type u.Pos|nil
|
---@type Pos|nil
|
||||||
local curr = self
|
local curr = self
|
||||||
while curr ~= nil do
|
while curr ~= nil do
|
||||||
if predicate(curr) then return curr end
|
if predicate(curr) then return curr end
|
||||||
@@ -227,14 +186,12 @@ function Pos:find_next(dir, predicate)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Finds the matching bracket/paren for the current position.
|
--- Finds the matching bracket/paren for the current position.
|
||||||
--- @param max_chars? number|nil
|
---@param max_chars? number|nil
|
||||||
--- @param invocations? u.Pos[]
|
---@param invocations? Pos[]
|
||||||
--- @return u.Pos|nil
|
---@return 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
|
if vim.tbl_contains(invocations, function(p) return self == p end, { predicate = true }) then return nil end
|
||||||
return nil
|
|
||||||
end
|
|
||||||
table.insert(invocations, self)
|
table.insert(invocations, self)
|
||||||
|
|
||||||
local openers = { '{', '[', '(', '<' }
|
local openers = { '{', '[', '(', '<' }
|
||||||
@@ -244,20 +201,12 @@ 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
|
local i, _ = vim.iter(is_opener and openers or closers):enumerate():find(function(_, c2) return c == c2 end)
|
||||||
.iter(is_opener and openers or closers)
|
|
||||||
:enumerate()
|
|
||||||
:find(function(_, c2) return c == c2 end)
|
|
||||||
-- 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]
|
||||||
|
|
||||||
--- @type u.Pos|nil
|
---@type Pos|nil
|
||||||
local cur = self
|
local cur = self
|
||||||
--- `adv` is a helper that moves the current position backward or forward,
|
---@return Pos|nil
|
||||||
--- depending on whether we are looking for an opener or a closer. It returns
|
|
||||||
--- nil if 1) the watch-dog `max_chars` falls bellow 0, or 2) if we have gone
|
|
||||||
--- beyond the beginning/end of the file.
|
|
||||||
--- @return u.Pos|nil
|
|
||||||
local function adv()
|
local function adv()
|
||||||
if cur == nil then return nil end
|
if cur == nil then return nil end
|
||||||
|
|
||||||
@@ -269,7 +218,7 @@ function Pos:find_match(max_chars, invocations)
|
|||||||
return cur:next(is_opener and 1 or -1)
|
return cur:next(is_opener and 1 or -1)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- scan until we find `c_match`:
|
-- scan until we find a match:
|
||||||
cur = adv()
|
cur = adv()
|
||||||
while cur ~= nil and cur:char() ~= c_match do
|
while cur ~= nil and cur:char() ~= c_match do
|
||||||
cur = adv()
|
cur = adv()
|
||||||
@@ -287,17 +236,4 @@ function Pos:find_match(max_chars, invocations)
|
|||||||
return cur
|
return cur
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param lines string|string[]
|
|
||||||
function Pos:insert_before(lines)
|
|
||||||
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
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Pos
|
return Pos
|
||||||
|
|||||||
794
lua/u/range.lua
794
lua/u/range.lua
@@ -1,80 +1,62 @@
|
|||||||
local Extmark = require 'u.extmark'
|
|
||||||
local Pos = require 'u.pos'
|
local Pos = require 'u.pos'
|
||||||
|
local State = require 'u.state'
|
||||||
|
|
||||||
local ESC = vim.api.nvim_replace_termcodes('<Esc>', true, false, true)
|
local orig_on_yank = (vim.hl or vim.highlight).on_yank
|
||||||
local NS = vim.api.nvim_create_namespace 'u.range'
|
local on_yank_enabled = true;
|
||||||
|
((vim.hl or vim.highlight) --[[@as any]]).on_yank = function(opts)
|
||||||
--- @class u.Range
|
if not on_yank_enabled then return end
|
||||||
--- @field start u.Pos
|
return orig_on_yank(opts)
|
||||||
--- @field stop u.Pos|nil
|
|
||||||
--- @field mode 'v'|'V'
|
|
||||||
local Range = {}
|
|
||||||
Range.__index = Range
|
|
||||||
function Range.__tostring(self)
|
|
||||||
--- @param p u.Pos
|
|
||||||
local function posstr(p)
|
|
||||||
if p == nil then
|
|
||||||
return 'nil'
|
|
||||||
elseif p.off ~= 0 then
|
|
||||||
return string.format('Pos(%d:%d){off=%d}', p.lnum, p.col, p.off)
|
|
||||||
else
|
|
||||||
return string.format('Pos(%d:%d)', p.lnum, p.col)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local _1 = posstr(self.start)
|
|
||||||
local _2 = posstr(self.stop)
|
|
||||||
return string.format(
|
|
||||||
'Range{bufnr=%d, mode=%s, start=%s, stop=%s}',
|
|
||||||
self.start.bufnr,
|
|
||||||
self.mode,
|
|
||||||
_1,
|
|
||||||
_2
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
---@class Range
|
||||||
-- Range constructors:
|
---@field start Pos
|
||||||
--------------------------------------------------------------------------------
|
---@field stop Pos|nil
|
||||||
|
---@field mode 'v'|'V'
|
||||||
|
local Range = {}
|
||||||
|
|
||||||
--- @param start u.Pos
|
---@param start Pos
|
||||||
--- @param stop u.Pos|nil
|
---@param stop Pos|nil
|
||||||
--- @param mode? 'v'|'V'
|
---@param mode? 'v'|'V'
|
||||||
--- @return u.Range
|
---@return Range
|
||||||
function Range.new(start, stop, mode)
|
function Range.new(start, stop, mode)
|
||||||
if stop ~= nil and stop < start then
|
if stop ~= nil and stop < start then
|
||||||
start, stop = stop, start
|
start, stop = stop, start
|
||||||
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 Pos
|
||||||
|
local function posstr(p)
|
||||||
|
if p == nil then
|
||||||
|
return 'nil'
|
||||||
|
elseif p.off ~= 0 then
|
||||||
|
return string.format('Pos(%d:%d){off=%d}', p.lnum, p.col, p.off)
|
||||||
|
else
|
||||||
|
return string.format('Pos(%d:%d)', p.lnum, p.col)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
setmetatable(r, Range)
|
local _1 = posstr(r.start)
|
||||||
|
local _2 = posstr(r.stop)
|
||||||
|
return string.format('Range{buf=%d, mode=%s, start=%s, stop=%s}', r.start.buf, r.mode, _1, _2)
|
||||||
|
end
|
||||||
|
setmetatable(r, { __index = Range, __tostring = str })
|
||||||
return r
|
return r
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param ranges (u.Range|nil)[]
|
function Range.is(x)
|
||||||
function Range.smallest(ranges)
|
local mt = getmetatable(x)
|
||||||
--- @type u.Range[]
|
return mt and mt.__index == 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
|
||||||
--- @param rpos string
|
---@param rpos string
|
||||||
--- @return u.Range
|
---@return Range
|
||||||
function Range.from_marks(lpos, rpos)
|
function Range.from_marks(lpos, rpos)
|
||||||
local start = Pos.from_pos(lpos)
|
local start = Pos.from_pos(lpos)
|
||||||
local stop = Pos.from_pos(rpos)
|
local stop = Pos.from_pos(rpos)
|
||||||
|
|
||||||
--- @type 'v'|'V'
|
---@type 'v'|'V'
|
||||||
local mode
|
local mode
|
||||||
if stop:is_col_max() then
|
if stop:is_col_max() then
|
||||||
mode = 'V'
|
mode = 'V'
|
||||||
@@ -85,137 +67,101 @@ function Range.from_marks(lpos, rpos)
|
|||||||
return Range.new(start, stop, mode)
|
return Range.new(start, stop, mode)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param bufnr? number
|
---@param buf? number
|
||||||
function Range.from_buf_text(bufnr)
|
function Range.from_buf_text(buf)
|
||||||
if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
|
if buf == nil or buf == 0 then buf = vim.api.nvim_get_current_buf() end
|
||||||
local num_lines = vim.api.nvim_buf_line_count(bufnr)
|
local num_lines = vim.api.nvim_buf_line_count(buf)
|
||||||
|
|
||||||
local start = Pos.new(bufnr, 1, 1)
|
local start = Pos.new(buf, 0, 0)
|
||||||
local stop = Pos.new(bufnr, num_lines, Pos.MAX_COL)
|
local stop = Pos.new(buf, num_lines - 1, Pos.MAX_COL)
|
||||||
return Range.new(start, stop, 'V')
|
return Range.new(start, stop, 'V')
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param bufnr? number
|
---@param buf? number
|
||||||
--- @param line number 1-based line index
|
---@param line number 0-based line index
|
||||||
function Range.from_line(bufnr, line) return Range.from_lines(bufnr, line, line) end
|
function Range.from_line(buf, line) return Range.from_lines(buf, line, line) end
|
||||||
|
|
||||||
--- @param bufnr? number
|
---@param buf? number
|
||||||
--- @param start_line number based line index
|
---@param start_line number 0-based line index
|
||||||
--- @param stop_line number based line index
|
---@param stop_line number 0-based line index
|
||||||
function Range.from_lines(bufnr, start_line, stop_line)
|
function Range.from_lines(buf, start_line, stop_line)
|
||||||
if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
|
if buf == nil or buf == 0 then buf = vim.api.nvim_get_current_buf() end
|
||||||
if stop_line < 0 then
|
if stop_line < 0 then
|
||||||
local num_lines = vim.api.nvim_buf_line_count(bufnr)
|
local num_lines = vim.api.nvim_buf_line_count(buf)
|
||||||
stop_line = num_lines + stop_line + 1
|
stop_line = num_lines + stop_line
|
||||||
end
|
end
|
||||||
return Range.new(Pos.new(bufnr, start_line, 1), Pos.new(bufnr, stop_line, Pos.MAX_COL), 'V')
|
return Range.new(Pos.new(buf, start_line, 0), Pos.new(buf, stop_line, Pos.MAX_COL), 'V')
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param motion string
|
---@param text_obj string
|
||||||
--- @param opts? { bufnr?: number; contains_cursor?: boolean; pos?: u.Pos, user_defined?: boolean }
|
---@param opts? { buf?: number; contains_cursor?: boolean; pos?: Pos, user_defined?: boolean }
|
||||||
--- @return u.Range|nil
|
---@return Range|nil
|
||||||
function Range.from_motion(motion, opts)
|
function Range.from_text_object(text_obj, opts)
|
||||||
-- Options handling:
|
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
if opts.bufnr == nil or opts.bufnr == 0 then opts.bufnr = vim.api.nvim_get_current_buf() end
|
if opts.buf == nil then opts.buf = 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
|
||||||
|
|
||||||
-- Extract some information from the motion:
|
---@type "a" | "i"
|
||||||
--- @type 'a'|'i', string
|
local selection_type = text_obj:sub(1, 1)
|
||||||
local scope, motion_rest = motion:sub(1, 1), motion:sub(2)
|
local obj_type = text_obj:sub(#text_obj, #text_obj)
|
||||||
local is_txtobj = scope == 'a' or scope == 'i'
|
local is_quote = vim.tbl_contains({ "'", '"', '`' }, obj_type)
|
||||||
local is_quote_txtobj = is_txtobj and vim.tbl_contains({ "'", '"', '`' }, motion_rest)
|
|
||||||
|
|
||||||
-- Capture the original state of the buffer for restoration later.
|
|
||||||
local original_state = {
|
|
||||||
winview = vim.fn.winsaveview(),
|
|
||||||
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()
|
|
||||||
if opts.pos ~= nil then opts.pos:save_to_pos '.' end
|
|
||||||
|
|
||||||
_G.Range__from_motion_opfunc = function(ty)
|
|
||||||
_G.Range__from_motion_opfunc_captured_range = Range.from_op_func(ty)
|
|
||||||
end
|
|
||||||
local old_eventignore = vim.o.eventignore
|
|
||||||
vim.o.eventignore = 'all'
|
|
||||||
vim.go.operatorfunc = 'v:lua.Range__from_motion_opfunc'
|
|
||||||
vim.cmd {
|
|
||||||
cmd = 'normal',
|
|
||||||
bang = not opts.user_defined,
|
|
||||||
args = { ESC .. 'g@' .. motion },
|
|
||||||
mods = { silent = true },
|
|
||||||
}
|
|
||||||
vim.o.eventignore = old_eventignore
|
|
||||||
end)
|
|
||||||
local captured_range = _G.Range__from_motion_opfunc_captured_range
|
|
||||||
|
|
||||||
-- Restore original state:
|
|
||||||
vim.fn.winrestview(original_state.winview)
|
|
||||||
vim.fn.setreg('"', original_state.regquote)
|
|
||||||
vim.fn.setpos('.', original_state.cursor)
|
|
||||||
vim.fn.setpos("'[", original_state.pos_lbrack)
|
|
||||||
vim.fn.setpos("']", original_state.pos_rbrack)
|
|
||||||
if original_state.prev_mode ~= 'n' then original_state.vinf:set_visual_selection() end
|
|
||||||
vim.go.operatorfunc = original_state.opfunc
|
|
||||||
_G.Range__from_motion_opfunc_captured_range = original_state.prev_captured_range
|
|
||||||
|
|
||||||
if not captured_range then return nil end
|
|
||||||
|
|
||||||
-- 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
|
|
||||||
|
|
||||||
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 '.'
|
local cursor = Pos.from_pos '.'
|
||||||
return vim.tbl_map(function(cap_ranges)
|
|
||||||
return vim
|
-- Yank, then read '[ and '] to know the bounds:
|
||||||
.iter(cap_ranges)
|
---@type { start: Pos; stop: Pos }
|
||||||
:filter(
|
local positions
|
||||||
--- @param r u.Range
|
vim.api.nvim_buf_call(opts.buf, function()
|
||||||
function(r) return r:contains(cursor) end
|
positions = State.run(0, function(s)
|
||||||
)
|
s:track_winview()
|
||||||
:totable()
|
s:track_register '"'
|
||||||
end, ranges)
|
s:track_pos '.'
|
||||||
|
s:track_pos "'["
|
||||||
|
s:track_pos "']"
|
||||||
|
|
||||||
|
if opts.pos ~= nil then opts.pos:save_to_pos '.' end
|
||||||
|
|
||||||
|
local null_pos = Pos.new(0, 0, 0, 0)
|
||||||
|
null_pos:save_to_pos "'["
|
||||||
|
null_pos:save_to_pos "']"
|
||||||
|
|
||||||
|
local prev_on_yank_enabled = on_yank_enabled
|
||||||
|
on_yank_enabled = false
|
||||||
|
vim.cmd {
|
||||||
|
cmd = 'normal',
|
||||||
|
bang = not opts.user_defined,
|
||||||
|
args = { '""y' .. text_obj },
|
||||||
|
mods = { silent = true },
|
||||||
|
}
|
||||||
|
on_yank_enabled = prev_on_yank_enabled
|
||||||
|
|
||||||
|
local start = Pos.from_pos "'["
|
||||||
|
local stop = Pos.from_pos "']"
|
||||||
|
|
||||||
|
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
|
||||||
|
return { start = start, stop = stop }
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
local start = positions.start
|
||||||
|
local stop = positions.stop
|
||||||
|
if start == stop and start.lnum == 0 and start.col == 0 and start.off == 0 then return nil end
|
||||||
|
if opts.contains_cursor and not Range.new(start, stop):contains(cursor) then return nil end
|
||||||
|
|
||||||
|
if is_quote and selection_type == 'a' then
|
||||||
|
start = start:find_next(1, obj_type) or start
|
||||||
|
stop = stop:find_next(-1, obj_type) or stop
|
||||||
|
end
|
||||||
|
|
||||||
|
return Range.new(start, stop)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Get range information from the currently selected visual text.
|
--- Get range information from the currently selected visual text.
|
||||||
@@ -232,7 +178,7 @@ end
|
|||||||
--- Get range information from the current text range being operated on
|
--- Get range information from the current text range being operated on
|
||||||
--- as defined by an operator-pending function. Infers line-wise vs. char-wise
|
--- as defined by an operator-pending function. Infers line-wise vs. char-wise
|
||||||
--- based on the type, as given by the operator-pending function.
|
--- based on the type, as given by the operator-pending function.
|
||||||
--- @param type 'line'|'char'|'block'
|
---@param type 'line'|'char'|'block'
|
||||||
function Range.from_op_func(type)
|
function Range.from_op_func(type)
|
||||||
if type == 'block' then error 'block motions not supported' end
|
if type == 'block' then error 'block motions not supported' end
|
||||||
|
|
||||||
@@ -242,200 +188,91 @@ function Range.from_op_func(type)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Get range information from command arguments.
|
--- Get range information from command arguments.
|
||||||
--- @param args unknown
|
---@param args unknown
|
||||||
--- @return u.Range|nil
|
---@return Range|nil
|
||||||
function Range.from_cmd_args(args)
|
function Range.from_cmd_args(args)
|
||||||
if args.range == 0 then return nil end
|
---@type 'v'|'V'
|
||||||
|
local mode
|
||||||
local bufnr = vim.api.nvim_get_current_buf()
|
---@type nil|Pos
|
||||||
if args.range == 1 then
|
local start
|
||||||
return Range.new(Pos.new(bufnr, args.line1, 1), Pos.new(bufnr, args.line1, Pos.MAX_COL), 'V')
|
local stop
|
||||||
end
|
if args.range == 0 then
|
||||||
|
return nil
|
||||||
local is_visual = vim.fn.histget('cmd', -1):sub(1, 5) == [['<,'>]]
|
|
||||||
--- @type 'v'|'V'
|
|
||||||
local mode = is_visual and vim.fn.visualmode() or 'V'
|
|
||||||
|
|
||||||
if is_visual then
|
|
||||||
return Range.new(Pos.from_pos "'<", Pos.from_pos "'>", mode)
|
|
||||||
else
|
else
|
||||||
return Range.new(Pos.new(bufnr, args.line1, 1), Pos.new(bufnr, args.line2, Pos.MAX_COL), mode)
|
start = Pos.from_pos "'<"
|
||||||
|
stop = Pos.from_pos "'>"
|
||||||
|
if stop:is_col_max() then
|
||||||
|
mode = 'V'
|
||||||
|
else
|
||||||
|
mode = 'v'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
return Range.new(start, stop, mode)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---
|
||||||
function Range.find_nearest_brackets()
|
function Range.find_nearest_brackets()
|
||||||
return Range.smallest {
|
local a = Range.from_text_object('a<', { contains_cursor = true })
|
||||||
Range.from_motion('a<', { contains_cursor = true }),
|
local b = Range.from_text_object('a[', { contains_cursor = true })
|
||||||
Range.from_motion('a[', { contains_cursor = true }),
|
local c = Range.from_text_object('a(', { contains_cursor = true })
|
||||||
Range.from_motion('a(', { contains_cursor = true }),
|
local d = Range.from_text_object('a{', { contains_cursor = true })
|
||||||
Range.from_motion('a{', { contains_cursor = true }),
|
return Range.smallest { a, b, c, d }
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Range.find_nearest_quotes()
|
function Range.find_nearest_quotes()
|
||||||
return Range.smallest {
|
local a = Range.from_text_object([[a']], { contains_cursor = true })
|
||||||
Range.from_motion([[a']], { contains_cursor = true }),
|
if a ~= nil and a:is_empty() then a = nil end
|
||||||
Range.from_motion([[a"]], { contains_cursor = true }),
|
local b = Range.from_text_object([[a"]], { contains_cursor = true })
|
||||||
Range.from_motion([[a`]], { contains_cursor = true }),
|
if b ~= nil and b:is_empty() then b = nil end
|
||||||
}
|
local c = Range.from_text_object([[a`]], { contains_cursor = true })
|
||||||
|
if c ~= nil and c:is_empty() then c = nil end
|
||||||
|
return Range.smallest { a, b, c }
|
||||||
end
|
end
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
---@param ranges (Range|nil)[]
|
||||||
-- Structural utilities:
|
function Range.smallest(ranges)
|
||||||
--------------------------------------------------------------------------------
|
---@type Range[]
|
||||||
|
local new_ranges = {}
|
||||||
|
for _, r in pairs(ranges) do
|
||||||
|
if r ~= nil then table.insert(new_ranges, r) end
|
||||||
|
end
|
||||||
|
ranges = new_ranges
|
||||||
|
if #ranges == 0 then return nil end
|
||||||
|
|
||||||
function Range:clone()
|
-- find smallest match
|
||||||
return Range.new(self.start:clone(), self.stop ~= nil and self.stop:clone() or nil, self.mode)
|
local max_start = ranges[1].start
|
||||||
|
local min_stop = ranges[1].stop
|
||||||
|
local result = ranges[1]
|
||||||
|
|
||||||
|
for _, r in ipairs(ranges) do
|
||||||
|
local start, stop = r.start, r.stop
|
||||||
|
if start > max_start and stop < min_stop then
|
||||||
|
max_start = start
|
||||||
|
min_stop = stop
|
||||||
|
result = r
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
function Range:is_empty() return self.stop == nil end
|
function Range:clone() return Range.new(self.start:clone(), self.stop ~= nil and self.stop:clone() or nil, self.mode) 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()
|
||||||
|
|
||||||
r.mode = 'V'
|
r.mode = 'V'
|
||||||
r.start.col = 1
|
r.start.col = 0
|
||||||
if r.stop ~= nil then r.stop.col = Pos.MAX_COL end
|
if r.stop ~= nil then r.stop.col = Pos.MAX_COL end
|
||||||
|
|
||||||
return r
|
return r
|
||||||
end
|
end
|
||||||
|
|
||||||
function Range:to_charwise()
|
function Range:is_empty() return self.stop == nil end
|
||||||
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
|
||||||
@@ -461,135 +298,116 @@ function Range:trim_stop()
|
|||||||
return r
|
return r
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param i number 1-based
|
---@param p Pos
|
||||||
--- @param j? number 1-based
|
function Range:contains(p) return not self:is_empty() and p >= self.start and p <= self.stop end
|
||||||
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 string[]
|
||||||
--- @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[]
|
|
||||||
function Range:lines()
|
function Range:lines()
|
||||||
if self:is_empty() then return {} end
|
if self:is_empty() then return {} end
|
||||||
return vim.fn.getregion(self.start:as_vim(), self.stop:as_vim(), { type = self.mode })
|
|
||||||
|
local lines = {}
|
||||||
|
for i = 0, self.stop.lnum - self.start.lnum do
|
||||||
|
local line = self:line0(i)
|
||||||
|
if line ~= nil then table.insert(lines, line.text()) end
|
||||||
|
end
|
||||||
|
return lines
|
||||||
end
|
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 l number
|
---@param i number 1-based
|
||||||
-- luacheck: ignore
|
---@param j? number 1-based
|
||||||
--- @return { line: string; idx0: { start: number; stop: number; }; lnum: number; range: fun():u.Range; text: fun():string }|nil
|
function Range:sub(i, j) return self:text():sub(i, j) end
|
||||||
function Range:line(l)
|
|
||||||
if l < 0 then l = self:line_count() + l + 1 end
|
---@param l number
|
||||||
|
---@return { line: string; idx0: { start: number; stop: number; }; lnum: number; range: fun():Range; text: fun():string }|nil
|
||||||
|
function Range:line0(l)
|
||||||
|
if l < 0 then return self:line0(self:line_count() + l) end
|
||||||
if l > self:line_count() then return end
|
if l > self:line_count() then return end
|
||||||
|
|
||||||
local line_indices =
|
local line = vim.api.nvim_buf_get_lines(self.start.buf, self.start.lnum + l, self.start.lnum + l + 1, false)[1]
|
||||||
vim.fn.getregionpos(self.start:as_vim(), self.stop:as_vim(), { type = self.mode })
|
if line == nil then return end
|
||||||
local line_bounds = line_indices[l]
|
|
||||||
|
|
||||||
local start = Pos.new(unpack(line_bounds[1]))
|
local start = 0
|
||||||
local stop = Pos.new(unpack(line_bounds[2]))
|
local stop = #line - 1
|
||||||
return Range.new(start, stop)
|
if l == 0 then start = self.start.col end
|
||||||
|
if l == self.stop.lnum - self.start.lnum then stop = self.stop.col end
|
||||||
|
if stop == Pos.MAX_COL then stop = #line - 1 end
|
||||||
|
local lnum = self.start.lnum + l
|
||||||
|
|
||||||
|
return {
|
||||||
|
line = line,
|
||||||
|
idx0 = { start = start, stop = stop },
|
||||||
|
lnum = lnum,
|
||||||
|
range = function()
|
||||||
|
return Range.new(
|
||||||
|
Pos.new(self.start.buf, lnum, start, self.start.off),
|
||||||
|
Pos.new(self.start.buf, lnum, stop, self.stop.off),
|
||||||
|
'v'
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
text = function() return line:sub(start + 1, stop + 1) end,
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param replacement nil|string|string[]
|
---@param replacement nil|string|string[]
|
||||||
function Range:replace(replacement)
|
function Range:replace(replacement)
|
||||||
if replacement == nil then replacement = {} end
|
if replacement == nil then replacement = {} end
|
||||||
if type(replacement) == 'string' then replacement = vim.fn.split(replacement, '\n') end
|
if type(replacement) == 'string' then replacement = vim.fn.split(replacement, '\n') end
|
||||||
|
|
||||||
local bufnr = self.start.bufnr
|
local buf = self.start.buf
|
||||||
|
-- convert to start-inclusive, stop-exclusive coordinates:
|
||||||
|
local start_lnum, stop_lnum = self.start.lnum, (self.stop and self.stop.lnum or self.start.lnum) + 1
|
||||||
|
local start_col, stop_col = self.start.col, (self.stop and self.stop.col or self.start.col) + 1
|
||||||
|
|
||||||
local replace_type = (self:is_empty() and 'insert') or (self.mode == 'v' and 'region') or 'lines'
|
local replace_type = (self:is_empty() and 'insert') or (self.mode == 'v' and 'region') or 'lines'
|
||||||
|
|
||||||
local function update_stop_non_linewise()
|
---@param alnum number
|
||||||
|
---@param acol number
|
||||||
|
---@param blnum number
|
||||||
|
---@param bcol number
|
||||||
|
local function set_text(alnum, acol, blnum, bcol, repl)
|
||||||
|
-- row indices are end-inclusive, and column indices are end-exclusive.
|
||||||
|
vim.api.nvim_buf_set_text(buf, alnum, acol, blnum, bcol, repl)
|
||||||
|
|
||||||
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
|
if new_last_line_num == start_lnum then new_last_col = new_last_col + start_col - 1 end
|
||||||
new_last_col = new_last_col + self.start.col - 1
|
|
||||||
end
|
self.stop = Pos.new(buf, 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()
|
|
||||||
if #replacement == 0 then
|
---@param alnum number
|
||||||
|
---@param blnum number
|
||||||
|
local function set_lines(alnum, blnum, repl)
|
||||||
|
-- indexing is zero-based, end-exclusive
|
||||||
|
vim.api.nvim_buf_set_lines(buf, alnum, blnum, false, repl)
|
||||||
|
|
||||||
|
if #repl == 0 then
|
||||||
self.stop = nil
|
self.stop = nil
|
||||||
else
|
else
|
||||||
local new_last_line_num = self.start.lnum - 1 + #replacement - 1
|
local new_last_line_num = start_lnum + #replacement - 1
|
||||||
self.stop = Pos.new(bufnr, new_last_line_num + 1, Pos.MAX_COL, self.stop.off)
|
self.stop = Pos.new(self.start.buf, new_last_line_num, Pos.MAX_COL, self.stop.off)
|
||||||
end
|
end
|
||||||
self.mode = 'v'
|
self.mode = 'v'
|
||||||
end
|
end
|
||||||
|
|
||||||
if replace_type == 'insert' then
|
if replace_type == 'insert' then
|
||||||
-- To insert text at a given `(row, column)` location, use `start_row =
|
set_text(start_lnum, start_col, start_lnum, start_col, replacement)
|
||||||
-- end_row = row` and `start_col = end_col = col`.
|
|
||||||
vim.api.nvim_buf_set_text(
|
|
||||||
bufnr,
|
|
||||||
self.start.lnum - 1,
|
|
||||||
self.start.col - 1,
|
|
||||||
self.start.lnum - 1,
|
|
||||||
self.start.col - 1,
|
|
||||||
replacement
|
|
||||||
)
|
|
||||||
update_stop_non_linewise()
|
|
||||||
elseif replace_type == 'region' then
|
elseif replace_type == 'region' then
|
||||||
-- Fixup the bounds:
|
-- Fixup the bounds:
|
||||||
local max_col = #self.stop:line()
|
local last_line = vim.api.nvim_buf_get_lines(buf, stop_lnum - 1, stop_lnum, false)[1] or ''
|
||||||
|
local max_col = #last_line
|
||||||
-- Indexing is zero-based. Row indices are end-inclusive, and column indices
|
set_text(start_lnum, start_col, stop_lnum - 1, math.min(stop_col, max_col), replacement)
|
||||||
-- are end-exclusive.
|
|
||||||
vim.api.nvim_buf_set_text(
|
|
||||||
bufnr,
|
|
||||||
self.start.lnum - 1,
|
|
||||||
self.start.col - 1,
|
|
||||||
self.stop.lnum - 1,
|
|
||||||
math.min(self.stop.col, max_col),
|
|
||||||
replacement
|
|
||||||
)
|
|
||||||
update_stop_non_linewise()
|
|
||||||
elseif replace_type == 'lines' then
|
elseif replace_type == 'lines' then
|
||||||
-- Indexing is zero-based, end-exclusive.
|
set_lines(start_lnum, stop_lnum, replacement)
|
||||||
vim.api.nvim_buf_set_lines(bufnr, self.start.lnum - 1, self.stop.lnum, true, replacement)
|
|
||||||
update_stop_linewise()
|
|
||||||
else
|
else
|
||||||
error 'unreachable'
|
error 'unreachable'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param amount number
|
---@param amount number
|
||||||
function Range:shrink(amount)
|
function Range:shrink(amount)
|
||||||
local start = self.start
|
local start = self.start
|
||||||
local stop = self.stop
|
local stop = self.stop
|
||||||
@@ -607,17 +425,61 @@ function Range:shrink(amount)
|
|||||||
return Range.new(start, stop, self.mode)
|
return Range.new(start, stop, self.mode)
|
||||||
end
|
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
|
if shrunk == nil or shrunk:is_empty() then error 'error in Range:must_shrink: Range:shrink() returned nil' end
|
||||||
error 'error in Range:must_shrink: Range:shrink() returned nil'
|
|
||||||
end
|
|
||||||
return shrunk
|
return shrunk
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param group string
|
---@param left string
|
||||||
--- @param opts? { timeout?: number, priority?: number, on_macro?: boolean }
|
---@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.buf then vim.api.nvim_set_current_buf(self.start.buf) end
|
||||||
|
|
||||||
|
State.run(self.start.buf, function(s)
|
||||||
|
s:track_mark 'a'
|
||||||
|
s:track_mark 'b'
|
||||||
|
|
||||||
|
self.start:save_to_mark 'a'
|
||||||
|
self.stop:save_to_mark 'b'
|
||||||
|
local mode = self.mode
|
||||||
|
|
||||||
|
local normal_cmd_args = ''
|
||||||
|
if vim.api.nvim_get_mode().mode == 'n' then normal_cmd_args = normal_cmd_args .. mode end
|
||||||
|
normal_cmd_args = normal_cmd_args .. '`ao`b'
|
||||||
|
vim.cmd { cmd = 'normal', args = { normal_cmd_args }, bang = true }
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param group string
|
||||||
|
---@param opts? { timeout?: number, priority?: number, on_macro?: boolean }
|
||||||
function Range:highlight(group, opts)
|
function Range:highlight(group, opts)
|
||||||
if self:is_empty() then return end
|
if self:is_empty() then return end
|
||||||
|
|
||||||
@@ -628,31 +490,33 @@ function Range:highlight(group, opts)
|
|||||||
if not opts.on_macro and in_macro then return { clear = function() end } end
|
if not opts.on_macro and in_macro then return { clear = function() end } end
|
||||||
|
|
||||||
local ns = vim.api.nvim_create_namespace ''
|
local ns = vim.api.nvim_create_namespace ''
|
||||||
|
State.run(self.start.buf, function(s)
|
||||||
|
if not in_macro then s:track_winview() end
|
||||||
|
|
||||||
local winview = vim.fn.winsaveview()
|
(vim.hl or vim.highlight).range(
|
||||||
vim.hl.range(
|
self.start.buf,
|
||||||
self.start.bufnr,
|
ns,
|
||||||
ns,
|
group,
|
||||||
group,
|
{ self.start.lnum, self.start.col },
|
||||||
{ self.start.lnum - 1, self.start.col - 1 },
|
{ self.stop.lnum, self.stop.col },
|
||||||
{ self.stop.lnum - 1, self.stop.col - 1 },
|
{
|
||||||
{
|
inclusive = true,
|
||||||
inclusive = true,
|
priority = opts.priority,
|
||||||
priority = opts.priority,
|
regtype = self.mode,
|
||||||
timeout = opts.timeout,
|
}
|
||||||
regtype = self.mode,
|
)
|
||||||
}
|
|
||||||
)
|
return nil
|
||||||
if not in_macro then vim.fn.winrestview(winview) end
|
end)
|
||||||
vim.cmd.redraw()
|
vim.cmd.redraw()
|
||||||
|
|
||||||
return {
|
local function clear()
|
||||||
ns = ns,
|
vim.api.nvim_buf_clear_namespace(self.start.buf, ns, self.start.lnum, self.stop.lnum + 1)
|
||||||
clear = function()
|
vim.cmd.redraw()
|
||||||
vim.api.nvim_buf_clear_namespace(self.start.bufnr, ns, self.start.lnum - 1, self.stop.lnum)
|
end
|
||||||
vim.cmd.redraw()
|
if opts.timeout ~= nil then vim.defer_fn(clear, opts.timeout) end
|
||||||
end,
|
|
||||||
}
|
return { ns = ns, clear = clear }
|
||||||
end
|
end
|
||||||
|
|
||||||
return Range
|
return Range
|
||||||
|
|||||||
@@ -1,73 +1,44 @@
|
|||||||
function _G.URendererOpFuncSwallow() end
|
local utils = require 'u.utils'
|
||||||
|
|
||||||
local ENABLE_LOG = false
|
|
||||||
|
|
||||||
local function log(...)
|
|
||||||
if not ENABLE_LOG then return end
|
|
||||||
|
|
||||||
local f = assert(io.open(vim.fs.joinpath(vim.fn.stdpath 'log', 'u.renderer.log'), 'a+'))
|
|
||||||
f:write(os.date() .. '\t' .. vim.inspect { ... } .. '\n')
|
|
||||||
f:close()
|
|
||||||
end
|
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
local H = {}
|
|
||||||
|
|
||||||
--- @alias u.renderer.TagEventHandler fun(tag: u.renderer.Tag, mode: string, lhs: string): string
|
--- @alias Tag { kind: 'tag'; name: string, attributes: table<string, unknown>, children: Tree }
|
||||||
|
--- @alias Node nil | boolean | string | Tag
|
||||||
|
--- @alias Tree Node | Node[]
|
||||||
|
local TagMetaTable = {}
|
||||||
|
|
||||||
--- @alias u.renderer.TagAttributes { [string]?: unknown; imap?: table<string, u.renderer.TagEventHandler>; nmap?: table<string, u.renderer.TagEventHandler>; vmap?: table<string, u.renderer.TagEventHandler>; xmap?: table<string, u.renderer.TagEventHandler>; omap?: table<string, u.renderer.TagEventHandler>, on_change?: fun(text: string): unknown }
|
--- @param name string
|
||||||
|
--- @param attributes? table<string, any>
|
||||||
|
--- @param children? Node | Node[]
|
||||||
|
--- @return Tag
|
||||||
|
function M.h(name, attributes, children)
|
||||||
|
return {
|
||||||
|
kind = 'tag',
|
||||||
|
name = name,
|
||||||
|
attributes = attributes or {},
|
||||||
|
children = children,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
--- @class u.renderer.Tag
|
--------------------------------------------------------------------------------
|
||||||
--- @field kind 'tag'
|
-- Renderer class
|
||||||
--- @field name string
|
--------------------------------------------------------------------------------
|
||||||
--- @field attributes u.renderer.TagAttributes
|
--- @alias RendererExtmark { id?: number; start: [number, number]; stop: [number, number]; opts: any; tag: any }
|
||||||
--- @field children u.renderer.Tree
|
|
||||||
|
|
||||||
--- @alias u.renderer.Node nil | boolean | string | u.renderer.Tag
|
--- @class Renderer
|
||||||
--- @alias u.renderer.Tree u.renderer.Node | u.renderer.Node[]
|
|
||||||
|
|
||||||
-- luacheck: ignore
|
|
||||||
--- @type table<string, fun(attributes: u.renderer.TagAttributes, children: u.renderer.Tree): u.renderer.Tag> & fun(name: string, attributes: u.renderer.TagAttributes, children: u.renderer.Tree): u.renderer.Tag>
|
|
||||||
M.h = setmetatable({}, {
|
|
||||||
__call = function(_, name, attributes, children)
|
|
||||||
return {
|
|
||||||
kind = 'tag',
|
|
||||||
name = name,
|
|
||||||
attributes = attributes or {},
|
|
||||||
children = children,
|
|
||||||
}
|
|
||||||
end,
|
|
||||||
__index = function(_, name)
|
|
||||||
return function(attributes, children)
|
|
||||||
return M.h('text', vim.tbl_deep_extend('force', { hl = name }, attributes), children)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Renderer {{{
|
|
||||||
--- @class u.renderer.RendererExtmark
|
|
||||||
--- @field id? number
|
|
||||||
--- @field start [number, number]
|
|
||||||
--- @field stop [number, number]
|
|
||||||
--- @field opts vim.api.keyset.set_extmark
|
|
||||||
--- @field tag u.renderer.Tag
|
|
||||||
|
|
||||||
--- @class u.renderer.Renderer
|
|
||||||
--- @field bufnr number
|
--- @field bufnr number
|
||||||
--- @field ns number
|
--- @field ns number
|
||||||
--- @field changedtick number
|
--- @field changedtick number
|
||||||
--- @field old { lines: string[]; extmarks: u.renderer.RendererExtmark[] }
|
--- @field old { lines: string[]; extmarks: RendererExtmark[] }
|
||||||
--- @field curr { lines: string[]; extmarks: u.renderer.RendererExtmark[] }
|
--- @field curr { lines: string[]; extmarks: RendererExtmark[] }
|
||||||
local Renderer = {}
|
local Renderer = {}
|
||||||
Renderer.__index = Renderer
|
Renderer.__index = Renderer
|
||||||
M.Renderer = Renderer
|
M.Renderer = Renderer
|
||||||
|
|
||||||
--- @private
|
|
||||||
--- @param x any
|
--- @param x any
|
||||||
--- @return boolean
|
--- @return boolean
|
||||||
function Renderer.is_tag(x) return type(x) == 'table' and x.kind == 'tag' end
|
function Renderer.is_tag(x) return type(x) == 'table' and x.kind == 'tag' end
|
||||||
|
|
||||||
--- @private
|
|
||||||
--- @param x any
|
--- @param x any
|
||||||
--- @return boolean
|
--- @return boolean
|
||||||
function Renderer.is_tag_arr(x)
|
function Renderer.is_tag_arr(x)
|
||||||
@@ -75,8 +46,8 @@ function Renderer.is_tag_arr(x)
|
|||||||
return #x == 0 or not Renderer.is_tag(x)
|
return #x == 0 or not Renderer.is_tag(x)
|
||||||
end
|
end
|
||||||
--- @param bufnr number|nil
|
--- @param bufnr number|nil
|
||||||
function Renderer.new(bufnr) -- {{{
|
function Renderer.new(bufnr)
|
||||||
if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
|
if bufnr == nil then bufnr = vim.api.nvim_get_current_buf() end
|
||||||
|
|
||||||
if vim.b[bufnr]._renderer_ns == nil then
|
if vim.b[bufnr]._renderer_ns == nil then
|
||||||
vim.b[bufnr]._renderer_ns = vim.api.nvim_create_namespace('my.renderer:' .. tostring(bufnr))
|
vim.b[bufnr]._renderer_ns = vim.api.nvim_create_namespace('my.renderer:' .. tostring(bufnr))
|
||||||
@@ -89,20 +60,14 @@ function Renderer.new(bufnr) -- {{{
|
|||||||
old = { lines = {}, extmarks = {} },
|
old = { lines = {}, extmarks = {} },
|
||||||
curr = { lines = {}, extmarks = {} },
|
curr = { lines = {}, extmarks = {} },
|
||||||
}, Renderer)
|
}, Renderer)
|
||||||
|
|
||||||
vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI', 'TextChangedP' }, {
|
|
||||||
buffer = bufnr,
|
|
||||||
callback = function() self:_on_text_changed() end,
|
|
||||||
})
|
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end -- }}}
|
end
|
||||||
|
|
||||||
--- @param opts {
|
--- @param opts {
|
||||||
--- tree: u.renderer.Tree;
|
--- tree: Tree;
|
||||||
--- on_tag?: fun(tag: u.renderer.Tag, start0: [number, number], stop0: [number, number]): any;
|
--- on_tag?: fun(tag: Tag, start0: [number, number], stop0: [number, number]): any;
|
||||||
--- }
|
--- }
|
||||||
function Renderer.markup_to_lines(opts) -- {{{
|
function Renderer.markup_to_lines(opts)
|
||||||
--- @type string[]
|
--- @type string[]
|
||||||
local lines = {}
|
local lines = {}
|
||||||
|
|
||||||
@@ -119,8 +84,8 @@ function Renderer.markup_to_lines(opts) -- {{{
|
|||||||
curr_col1 = 1
|
curr_col1 = 1
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param node u.renderer.Node
|
--- @param node Node
|
||||||
local function visit(node) -- {{{
|
local function visit(node)
|
||||||
if node == nil or type(node) == 'boolean' then return end
|
if node == nil or type(node) == 'boolean' then return end
|
||||||
|
|
||||||
if type(node) == 'string' then
|
if type(node) == 'string' then
|
||||||
@@ -134,13 +99,11 @@ function Renderer.markup_to_lines(opts) -- {{{
|
|||||||
|
|
||||||
-- visit the children:
|
-- visit the children:
|
||||||
if Renderer.is_tag_arr(node.children) then
|
if Renderer.is_tag_arr(node.children) then
|
||||||
for _, child in
|
for _, child in ipairs(node.children) do
|
||||||
ipairs(node.children --[[@as u.renderer.Node[] ]])
|
|
||||||
do
|
|
||||||
-- newlines are not controlled by array entries, do NOT output a line here:
|
-- newlines are not controlled by array entries, do NOT output a line here:
|
||||||
visit(child)
|
visit(child)
|
||||||
end
|
end
|
||||||
else -- luacheck: ignore
|
else
|
||||||
visit(node.children)
|
visit(node.children)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -152,92 +115,34 @@ function Renderer.markup_to_lines(opts) -- {{{
|
|||||||
visit(child)
|
visit(child)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end -- }}}
|
end
|
||||||
visit(opts.tree)
|
visit(opts.tree)
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
end -- }}}
|
end
|
||||||
|
|
||||||
--- @param opts {
|
--- @param opts {
|
||||||
--- tree: u.renderer.Tree;
|
--- tree: string;
|
||||||
--- format_tag?: fun(tag: u.renderer.Tag): string;
|
--- format_tag?: fun(tag: Tag): string;
|
||||||
--- }
|
--- }
|
||||||
function Renderer.markup_to_string(opts) return table.concat(Renderer.markup_to_lines(opts), '\n') end
|
function Renderer.markup_to_string(opts) return table.concat(Renderer.markup_to_lines(opts), '\n') end
|
||||||
|
|
||||||
--- @param bufnr number
|
--- @param tree Tree
|
||||||
--- @param old_lines string[] | nil
|
function Renderer:render(tree)
|
||||||
--- @param new_lines string[]
|
|
||||||
function Renderer.patch_lines(bufnr, old_lines, new_lines) -- {{{
|
|
||||||
--
|
|
||||||
-- Helpers:
|
|
||||||
--
|
|
||||||
|
|
||||||
--- @param start integer
|
|
||||||
--- @param end_ integer
|
|
||||||
--- @param strict_indexing boolean
|
|
||||||
--- @param replacement string[]
|
|
||||||
local function _set_lines(start, end_, strict_indexing, replacement)
|
|
||||||
vim.api.nvim_buf_set_lines(bufnr, start, end_, strict_indexing, replacement)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- @param start_row integer
|
|
||||||
--- @param start_col integer
|
|
||||||
--- @param end_row integer
|
|
||||||
--- @param end_col integer
|
|
||||||
--- @param replacement string[]
|
|
||||||
local function _set_text(start_row, start_col, end_row, end_col, replacement)
|
|
||||||
vim.api.nvim_buf_set_text(bufnr, start_row, start_col, end_row, end_col, replacement)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Morph the text to the desired state:
|
|
||||||
local line_changes =
|
|
||||||
H.levenshtein(old_lines or vim.api.nvim_buf_get_lines(bufnr, 0, -1, false), new_lines)
|
|
||||||
for _, line_change in ipairs(line_changes) do
|
|
||||||
local lnum0 = line_change.index - 1
|
|
||||||
|
|
||||||
if line_change.kind == 'add' then
|
|
||||||
_set_lines(lnum0, lnum0, true, { line_change.item })
|
|
||||||
elseif line_change.kind == 'change' then
|
|
||||||
-- Compute inter-line diff, and apply:
|
|
||||||
local col_changes =
|
|
||||||
H.levenshtein(vim.split(line_change.from, ''), vim.split(line_change.to, ''))
|
|
||||||
|
|
||||||
for _, col_change in ipairs(col_changes) do
|
|
||||||
local cnum0 = col_change.index - 1
|
|
||||||
if col_change.kind == 'add' then
|
|
||||||
_set_text(lnum0, cnum0, lnum0, cnum0, { col_change.item })
|
|
||||||
elseif col_change.kind == 'change' then
|
|
||||||
_set_text(lnum0, cnum0, lnum0, cnum0 + 1, { col_change.to })
|
|
||||||
elseif col_change.kind == 'delete' then
|
|
||||||
_set_text(lnum0, cnum0, lnum0, cnum0 + 1, {})
|
|
||||||
else -- luacheck: ignore
|
|
||||||
-- No change
|
|
||||||
end
|
|
||||||
end
|
|
||||||
elseif line_change.kind == 'delete' then
|
|
||||||
_set_lines(lnum0, lnum0 + 1, true, {})
|
|
||||||
else -- luacheck: ignore
|
|
||||||
-- No change
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end -- }}}
|
|
||||||
|
|
||||||
--- @param tree u.renderer.Tree
|
|
||||||
function Renderer:render(tree) -- {{{
|
|
||||||
local changedtick = vim.b[self.bufnr].changedtick
|
local changedtick = vim.b[self.bufnr].changedtick
|
||||||
if changedtick ~= self.changedtick then
|
if changedtick ~= self.changedtick then
|
||||||
self.curr = { lines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) }
|
self.curr = { lines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) }
|
||||||
self.changedtick = changedtick
|
self.changedtick = changedtick
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type u.renderer.RendererExtmark[]
|
--- @type RendererExtmark[]
|
||||||
local extmarks = {}
|
local extmarks = {}
|
||||||
|
|
||||||
--- @type string[]
|
--- @type string[]
|
||||||
local lines = Renderer.markup_to_lines {
|
local lines = Renderer.markup_to_lines {
|
||||||
tree = tree,
|
tree = tree,
|
||||||
|
|
||||||
on_tag = function(tag, start0, stop0) -- {{{
|
on_tag = function(tag, start0, stop0)
|
||||||
if tag.name == 'text' then
|
if tag.name == 'text' then
|
||||||
local hl = tag.attributes.hl
|
local hl = tag.attributes.hl
|
||||||
if type(hl) == 'string' then
|
if type(hl) == 'string' then
|
||||||
@@ -245,69 +150,116 @@ function Renderer:render(tree) -- {{{
|
|||||||
tag.attributes.extmark.hl_group = tag.attributes.extmark.hl_group or hl
|
tag.attributes.extmark.hl_group = tag.attributes.extmark.hl_group or hl
|
||||||
end
|
end
|
||||||
|
|
||||||
local extmark_opts = tag.attributes.extmark or {}
|
local extmark = tag.attributes.extmark
|
||||||
|
|
||||||
-- Set any necessary keymaps:
|
-- Set any necessary keymaps:
|
||||||
for _, mode in ipairs { 'i', 'n', 'v', 'x', 'o' } do
|
for _, mode in ipairs { 'i', 'n', 'v', 'x', 'o' } do
|
||||||
for lhs, _ in pairs(tag.attributes[mode .. 'map'] or {}) do
|
for lhs, _ in pairs(tag.attributes[mode .. 'map'] or {}) do
|
||||||
-- Force creating an extmark if there are key handlers. To accurately
|
-- Force creating an extmark if there are key handlers. To accurately
|
||||||
-- sense the bounds of the text, we need an extmark:
|
-- sense the bounds of the text, we need an extmark:
|
||||||
vim.keymap.set(mode, lhs, function()
|
extmark = extmark or {}
|
||||||
local result = self:_expr_map_callback(mode, lhs)
|
vim.keymap.set(
|
||||||
-- If the handler indicates that it wants to swallow the event,
|
'n',
|
||||||
-- we have to convert that intention into something compatible
|
lhs,
|
||||||
-- with expr-mappings, which don't support '<Nop>' (they try to
|
function() return self:_on_expr_map('n', lhs) end,
|
||||||
-- execute the literal characters). We'll use the 'g@' operator
|
{ buffer = self.bufnr, expr = true, replace_keycodes = true }
|
||||||
-- to do that, forwarding the event to an operatorfunc that does
|
)
|
||||||
-- nothing:
|
|
||||||
if result == '' then
|
|
||||||
if mode == 'i' then
|
|
||||||
return ''
|
|
||||||
else
|
|
||||||
vim.go.operatorfunc = 'v:lua.URendererOpFuncSwallow'
|
|
||||||
return 'g@ '
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return result
|
|
||||||
end, { buffer = self.bufnr, expr = true, replace_keycodes = true })
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
table.insert(extmarks, {
|
if extmark then
|
||||||
start = start0,
|
table.insert(extmarks, {
|
||||||
stop = stop0,
|
start = start0,
|
||||||
opts = extmark_opts,
|
stop = stop0,
|
||||||
tag = tag,
|
opts = extmark,
|
||||||
})
|
tag = tag,
|
||||||
|
})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end, -- }}}
|
end,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.old = self.curr
|
self.old = self.curr
|
||||||
self.curr = { lines = lines, extmarks = extmarks }
|
self.curr = { lines = lines, extmarks = extmarks }
|
||||||
self:_reconcile()
|
self:_reconcile()
|
||||||
vim.cmd.doautocmd { args = { 'User', 'Renderer:' .. tostring(self.bufnr) .. ':render' } }
|
end
|
||||||
end -- }}}
|
|
||||||
|
|
||||||
--- @private
|
--- @private
|
||||||
function Renderer:_reconcile() -- {{{
|
--- @param info string
|
||||||
|
--- @param start integer
|
||||||
|
--- @param end_ integer
|
||||||
|
--- @param strict_indexing boolean
|
||||||
|
--- @param replacement string[]
|
||||||
|
function Renderer:_set_lines(info, start, end_, strict_indexing, replacement)
|
||||||
|
self:_log { 'set_lines', self.bufnr, start, end_, strict_indexing, replacement }
|
||||||
|
vim.api.nvim_buf_set_lines(self.bufnr, start, end_, strict_indexing, replacement)
|
||||||
|
self:_log { 'after(' .. info .. ')', vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) }
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @private
|
||||||
|
--- @param info string
|
||||||
|
--- @param start_row integer
|
||||||
|
--- @param start_col integer
|
||||||
|
--- @param end_row integer
|
||||||
|
--- @param end_col integer
|
||||||
|
--- @param replacement string[]
|
||||||
|
function Renderer:_set_text(info, start_row, start_col, end_row, end_col, replacement)
|
||||||
|
self:_log { 'set_text', self.bufnr, start_row, start_col, end_row, end_col, replacement }
|
||||||
|
vim.api.nvim_buf_set_text(self.bufnr, start_row, start_col, end_row, end_col, replacement)
|
||||||
|
self:_log { 'after(' .. info .. ')', vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) }
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @private
|
||||||
|
function Renderer:_log(...)
|
||||||
|
--
|
||||||
|
-- vim.print(...)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @private
|
||||||
|
function Renderer:_reconcile()
|
||||||
|
local line_changes = utils.levenshtein(self.old.lines, self.curr.lines)
|
||||||
|
self.old = self.curr
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Step 1: morph the text to the desired state:
|
-- Step 1: morph the text to the desired state:
|
||||||
--
|
--
|
||||||
Renderer.patch_lines(self.bufnr, self.old.lines, self.curr.lines)
|
self:_log { line_changes = line_changes }
|
||||||
|
for _, line_change in ipairs(line_changes) do
|
||||||
|
local lnum0 = line_change.index - 1
|
||||||
|
|
||||||
|
if line_change.kind == 'add' then
|
||||||
|
self:_set_lines('add-line', lnum0, lnum0, true, { line_change.item })
|
||||||
|
elseif line_change.kind == 'change' then
|
||||||
|
-- Compute inter-line diff, and apply:
|
||||||
|
self:_log '--------------------------------------------------------------------------------'
|
||||||
|
local col_changes = utils.levenshtein(vim.split(line_change.from, ''), vim.split(line_change.to, ''))
|
||||||
|
|
||||||
|
for _, col_change in ipairs(col_changes) do
|
||||||
|
local cnum0 = col_change.index - 1
|
||||||
|
self:_log { line_change = col_change, cnum = cnum0, lnum = lnum0 }
|
||||||
|
if col_change.kind == 'add' then
|
||||||
|
self:_set_text('add-char', lnum0, cnum0, lnum0, cnum0, { col_change.item })
|
||||||
|
elseif col_change.kind == 'change' then
|
||||||
|
self:_set_text('change-char', lnum0, cnum0, lnum0, cnum0 + 1, { col_change.to })
|
||||||
|
elseif col_change.kind == 'delete' then
|
||||||
|
self:_set_text('del-char', lnum0, cnum0, lnum0, cnum0 + 1, {})
|
||||||
|
else
|
||||||
|
-- No change
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif line_change.kind == 'delete' then
|
||||||
|
self:_set_lines('del-line', lnum0, lnum0 + 1, true, {})
|
||||||
|
else
|
||||||
|
-- No change
|
||||||
|
end
|
||||||
|
end
|
||||||
self.changedtick = vim.b[self.bufnr].changedtick
|
self.changedtick = vim.b[self.bufnr].changedtick
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Step 2: reconcile extmarks:
|
-- Step 2: reconcile extmarks:
|
||||||
-- You may be tempted to try to keep track of which extmarks are needed, and
|
|
||||||
-- only delete those that are not needed. However, each time a tree is
|
|
||||||
-- rendered, brand new extmarks are created. For simplicity, it is better to
|
|
||||||
-- just delete all extmarks, and recreate them.
|
|
||||||
--
|
--
|
||||||
|
|
||||||
-- Clear current extmarks:
|
-- Clear current extmarks:
|
||||||
vim.api.nvim_buf_clear_namespace(self.bufnr, self.ns, 0, -1)
|
vim.api.nvim_buf_clear_namespace(self.bufnr, self.ns, 0, -1)
|
||||||
|
|
||||||
-- Set current extmarks:
|
-- Set current extmarks:
|
||||||
for _, extmark in ipairs(self.curr.extmarks) do
|
for _, extmark in ipairs(self.curr.extmarks) do
|
||||||
extmark.id = vim.api.nvim_buf_set_extmark(
|
extmark.id = vim.api.nvim_buf_set_extmark(
|
||||||
@@ -319,30 +271,19 @@ function Renderer:_reconcile() -- {{{
|
|||||||
id = extmark.id,
|
id = extmark.id,
|
||||||
end_row = extmark.stop[1],
|
end_row = extmark.stop[1],
|
||||||
end_col = extmark.stop[2],
|
end_col = extmark.stop[2],
|
||||||
-- If we change the text starting from the beginning (where the extmark
|
|
||||||
-- is), we don't want the extmark to move to the right.
|
|
||||||
right_gravity = false,
|
|
||||||
-- If we change the text starting from the end (where the end extmark
|
|
||||||
-- is), we don't want the extmark to stay stationary: we want it to
|
|
||||||
-- move to the right.
|
|
||||||
end_right_gravity = true,
|
|
||||||
}, extmark.opts)
|
}, extmark.opts)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
self.old = self.curr
|
|
||||||
end -- }}}
|
|
||||||
|
|
||||||
--- @private
|
--- @private
|
||||||
--- @param mode string
|
--- @param mode string
|
||||||
--- @param lhs string
|
--- @param lhs string
|
||||||
function Renderer:_expr_map_callback(mode, lhs) -- {{{
|
function Renderer:_on_expr_map(mode, lhs)
|
||||||
-- find the tag with the smallest intersection that contains the cursor:
|
-- find the tag with the smallest intersection that contains the cursor:
|
||||||
local pos0 = vim.api.nvim_win_get_cursor(0)
|
local pos0 = vim.api.nvim_win_get_cursor(0)
|
||||||
pos0[1] = pos0[1] - 1 -- make it actually 0-based
|
pos0[1] = pos0[1] - 1 -- make it actually 0-based
|
||||||
log('_expr_map_callback: pos0:', pos0)
|
local pos_infos = self:get_pos_infos(pos0)
|
||||||
local pos_infos = self:get_tags_at(pos0)
|
|
||||||
log('_expr_map_callback: pos_infos:', pos_infos)
|
|
||||||
|
|
||||||
if #pos_infos == 0 then return lhs end
|
if #pos_infos == 0 then return lhs end
|
||||||
|
|
||||||
@@ -352,10 +293,9 @@ function Renderer:_expr_map_callback(mode, lhs) -- {{{
|
|||||||
local tag = pos_info.tag
|
local tag = pos_info.tag
|
||||||
|
|
||||||
-- is the tag listening?
|
-- is the tag listening?
|
||||||
--- @type u.renderer.TagEventHandler?
|
|
||||||
local f = vim.tbl_get(tag.attributes, mode .. 'map', lhs)
|
local f = vim.tbl_get(tag.attributes, mode .. 'map', lhs)
|
||||||
if type(f) == 'function' then
|
if type(f) == 'function' then
|
||||||
local result = f(tag, mode, lhs)
|
local result = f()
|
||||||
if result == '' then
|
if result == '' then
|
||||||
-- bubble-up to the next tag, but set cancel to true, in case there are
|
-- bubble-up to the next tag, but set cancel to true, in case there are
|
||||||
-- no more tags to bubble up to:
|
-- no more tags to bubble up to:
|
||||||
@@ -368,166 +308,7 @@ function Renderer:_expr_map_callback(mode, lhs) -- {{{
|
|||||||
|
|
||||||
-- Resort to default behavior:
|
-- Resort to default behavior:
|
||||||
return cancel and '' or lhs
|
return cancel and '' or lhs
|
||||||
end -- }}}
|
end
|
||||||
|
|
||||||
function Renderer:_on_text_changed() -- {{{
|
|
||||||
-- Reset changedtick, so that the reconciler knows to refresh its cached
|
|
||||||
-- buffer-content before computing the diff:
|
|
||||||
self.changedtick = 0
|
|
||||||
|
|
||||||
--- @type integer, integer
|
|
||||||
local l, c = unpack(vim.api.nvim_win_get_cursor(0))
|
|
||||||
l = l - 1 -- make it actually 0-based
|
|
||||||
local pos_infos = self:get_tags_at({ l, c }, 'i')
|
|
||||||
log('_on_text_changed', { cursor_0_0 = { l, c }, pos_infos = pos_infos })
|
|
||||||
for _, pos_info in ipairs(pos_infos) do
|
|
||||||
local extmark_inf = pos_info.extmark
|
|
||||||
local tag = pos_info.tag
|
|
||||||
|
|
||||||
local on_change = tag.attributes.on_change
|
|
||||||
if on_change and type(on_change) == 'function' then
|
|
||||||
local extmark =
|
|
||||||
vim.api.nvim_buf_get_extmark_by_id(self.bufnr, self.ns, extmark_inf.id, { details = true })
|
|
||||||
|
|
||||||
--- @type integer, integer, vim.api.keyset.extmark_details
|
|
||||||
local start_row0, start_col0, details = unpack(extmark)
|
|
||||||
local end_row0, end_col0 = details.end_row, details.end_col
|
|
||||||
log('_on_text_changed: fetched current extmark for pos_info', {
|
|
||||||
pos_info = pos_info,
|
|
||||||
curr_extmark = {
|
|
||||||
start_row0 = start_row0,
|
|
||||||
start_col0 = start_col0,
|
|
||||||
end_row0 = end_row0,
|
|
||||||
end_col0 = end_col0,
|
|
||||||
details = details,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if start_row0 == end_row0 and start_col0 == end_col0 then
|
|
||||||
on_change ''
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local buf_max_line0 = math.max(1, vim.api.nvim_buf_line_count(self.bufnr) - 1)
|
|
||||||
if end_row0 > buf_max_line0 then
|
|
||||||
end_row0 = buf_max_line0
|
|
||||||
local last_line = vim.api.nvim_buf_get_lines(self.bufnr, end_row0, end_row0 + 1, false)[1]
|
|
||||||
or ''
|
|
||||||
end_col0 = last_line:len()
|
|
||||||
end
|
|
||||||
if end_col0 == 0 then
|
|
||||||
end_row0 = end_row0 - 1
|
|
||||||
local last_line = vim.api.nvim_buf_get_lines(self.bufnr, end_row0, end_row0 + 1, false)[1]
|
|
||||||
or ''
|
|
||||||
end_col0 = last_line:len()
|
|
||||||
end
|
|
||||||
log('_on_text_changed: after position correction', {
|
|
||||||
curr_extmark = {
|
|
||||||
start_row0 = start_row0,
|
|
||||||
start_col0 = start_col0,
|
|
||||||
end_row0 = end_row0,
|
|
||||||
end_col0 = end_col0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if start_row0 == end_row0 and start_col0 == end_col0 then
|
|
||||||
on_change ''
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local pos1 = { self.bufnr, start_row0 + 1, start_col0 + 1 }
|
|
||||||
local pos2 = { self.bufnr, end_row0 + 1, end_col0 }
|
|
||||||
local ok, lines = pcall(vim.fn.getregion, pos1, pos2, { type = 'v' })
|
|
||||||
if not ok then
|
|
||||||
log('_on_text_changed: getregion: invalid-pos ', {
|
|
||||||
{ pos1, pos2 },
|
|
||||||
})
|
|
||||||
vim.api.nvim_echo({
|
|
||||||
{ '(u.nvim:getregion:invalid-pos) ', 'ErrorMsg' },
|
|
||||||
{
|
|
||||||
'{ start, end } = ' .. vim.inspect({ pos1, pos2 }, { newline = ' ', indent = '' }),
|
|
||||||
},
|
|
||||||
}, true, {})
|
|
||||||
error(lines)
|
|
||||||
end
|
|
||||||
local text = table.concat(lines, '\n')
|
|
||||||
on_change(text)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end -- }}}
|
|
||||||
|
|
||||||
--- @private
|
|
||||||
function Renderer:_debug() -- {{{
|
|
||||||
local prev_w = vim.api.nvim_get_current_win()
|
|
||||||
vim.cmd.vnew()
|
|
||||||
local info_bufnr = vim.api.nvim_get_current_buf()
|
|
||||||
vim.bo.bufhidden = 'delete'
|
|
||||||
vim.bo.buflisted = false
|
|
||||||
vim.bo.buftype = 'nowrite'
|
|
||||||
|
|
||||||
local ids = {}
|
|
||||||
local function cleanup()
|
|
||||||
for _, id in ipairs(ids) do
|
|
||||||
vim.api.nvim_del_autocmd(id)
|
|
||||||
end
|
|
||||||
vim.api.nvim_buf_delete(info_bufnr, { force = true })
|
|
||||||
end
|
|
||||||
|
|
||||||
local function autocmd_callback()
|
|
||||||
if vim.api.nvim_get_current_win() ~= prev_w then return end
|
|
||||||
|
|
||||||
local l, c = unpack(vim.api.nvim_win_get_cursor(0))
|
|
||||||
l = l - 1 -- make it actually 0-based
|
|
||||||
|
|
||||||
local info = {
|
|
||||||
cursor = {
|
|
||||||
pos = { l, c },
|
|
||||||
tags = self:get_tags_at { l, c },
|
|
||||||
extmarks = vim.api.nvim_buf_get_extmarks(
|
|
||||||
self.bufnr,
|
|
||||||
self.ns,
|
|
||||||
{ l, c },
|
|
||||||
{ l, c },
|
|
||||||
{ details = true, overlap = true }
|
|
||||||
),
|
|
||||||
},
|
|
||||||
computed = {
|
|
||||||
extmarks = self.curr.extmarks,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
vim.api.nvim_buf_set_lines(info_bufnr, 0, -1, true, vim.split(vim.inspect(info), '\n'))
|
|
||||||
end
|
|
||||||
|
|
||||||
table.insert(
|
|
||||||
ids,
|
|
||||||
vim.api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI' }, {
|
|
||||||
callback = autocmd_callback,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
table.insert(
|
|
||||||
ids,
|
|
||||||
vim.api.nvim_create_autocmd({ 'User' }, {
|
|
||||||
pattern = 'Renderer:' .. tostring(self.bufnr) .. ':render',
|
|
||||||
callback = autocmd_callback,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
table.insert(
|
|
||||||
ids,
|
|
||||||
vim.api.nvim_create_autocmd('WinClosed', {
|
|
||||||
pattern = tostring(vim.api.nvim_get_current_win()),
|
|
||||||
callback = cleanup,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
table.insert(
|
|
||||||
ids,
|
|
||||||
vim.api.nvim_create_autocmd('WinClosed', {
|
|
||||||
pattern = tostring(prev_w),
|
|
||||||
callback = cleanup,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
vim.api.nvim_set_current_win(prev_w)
|
|
||||||
end -- }}}
|
|
||||||
|
|
||||||
--- Returns pairs of extmarks and tags associate with said extmarks. The
|
--- Returns pairs of extmarks and tags associate with said extmarks. The
|
||||||
--- returned tags/extmarks are sorted smallest (innermost) to largest
|
--- returned tags/extmarks are sorted smallest (innermost) to largest
|
||||||
@@ -535,32 +316,17 @@ end -- }}}
|
|||||||
---
|
---
|
||||||
--- @private (private for now)
|
--- @private (private for now)
|
||||||
--- @param pos0 [number; number]
|
--- @param pos0 [number; number]
|
||||||
--- @param mode string?
|
--- @return { extmark: RendererExtmark; tag: Tag; }[]
|
||||||
--- @return { extmark: u.renderer.RendererExtmark; tag: u.renderer.Tag; }[]
|
function Renderer:get_pos_infos(pos0)
|
||||||
function Renderer:get_tags_at(pos0, mode) -- {{{
|
|
||||||
local cursor_line0, cursor_col0 = pos0[1], pos0[2]
|
local cursor_line0, cursor_col0 = pos0[1], pos0[2]
|
||||||
if not mode then mode = vim.api.nvim_get_mode().mode end
|
|
||||||
mode = mode:sub(1, 1) -- we don't care about sub-modes
|
|
||||||
|
|
||||||
local raw_overlapping_extmarks = vim.api.nvim_buf_get_extmarks(
|
|
||||||
self.bufnr,
|
|
||||||
self.ns,
|
|
||||||
pos0,
|
|
||||||
pos0,
|
|
||||||
{ details = true, overlap = true }
|
|
||||||
)
|
|
||||||
log(
|
|
||||||
'get_tags_at: context:',
|
|
||||||
{ pos0 = pos0, mode = mode, raw_overlapping_extmarks = raw_overlapping_extmarks }
|
|
||||||
)
|
|
||||||
|
|
||||||
-- The cursor (block) occupies **two** extmark spaces: one for it's left
|
-- The cursor (block) occupies **two** extmark spaces: one for it's left
|
||||||
-- edge, and one for it's right. We need to do our own intersection test,
|
-- edge, and one for it's right. We need to do our own intersection test,
|
||||||
-- because the NeoVim API is over-inclusive in what it returns:
|
-- because the NeoVim API is over-inclusive in what it returns:
|
||||||
--- @type u.renderer.RendererExtmark[]
|
--- @type RendererExtmark[]
|
||||||
local mapped_extmarks = vim
|
local intersecting_extmarks = vim
|
||||||
.iter(raw_overlapping_extmarks)
|
.iter(vim.api.nvim_buf_get_extmarks(self.bufnr, self.ns, pos0, pos0, { details = true, overlap = true }))
|
||||||
--- @return u.renderer.RendererExtmark
|
--- @return RendererExtmark
|
||||||
:map(function(ext)
|
:map(function(ext)
|
||||||
--- @type number, number, number, { end_row?: number; end_col?: number }|nil
|
--- @type number, number, number, { end_row?: number; end_col?: number }|nil
|
||||||
local id, line0, col0, details = unpack(ext)
|
local id, line0, col0, details = unpack(ext)
|
||||||
@@ -571,43 +337,13 @@ function Renderer:get_tags_at(pos0, mode) -- {{{
|
|||||||
end
|
end
|
||||||
return { id = id, start = start, stop = stop, opts = details }
|
return { id = id, start = start, stop = stop, opts = details }
|
||||||
end)
|
end)
|
||||||
:totable()
|
--- @param ext RendererExtmark
|
||||||
|
|
||||||
local intersecting_extmarks = vim
|
|
||||||
.iter(mapped_extmarks)
|
|
||||||
--- @param ext u.renderer.RendererExtmark
|
|
||||||
:filter(function(ext)
|
:filter(function(ext)
|
||||||
if ext.stop[1] ~= nil and ext.stop[2] ~= nil then
|
if ext.stop[1] ~= nil and ext.stop[2] ~= nil then
|
||||||
-- If we've "ciw" and "collapsed" an extmark onto the cursor,
|
return cursor_line0 >= ext.start[1]
|
||||||
-- the cursor pos will equal the exmark's start AND end. In this
|
and cursor_col0 >= ext.start[2]
|
||||||
-- case, we want to include the extmark.
|
and cursor_line0 <= ext.stop[1]
|
||||||
if
|
and cursor_col0 < ext.stop[2]
|
||||||
cursor_line0 == ext.start[1]
|
|
||||||
and cursor_col0 == ext.start[2]
|
|
||||||
and cursor_line0 == ext.stop[1]
|
|
||||||
and cursor_col0 == ext.stop[2]
|
|
||||||
then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
return
|
|
||||||
-- START: line check
|
|
||||||
cursor_line0 >= ext.start[1]
|
|
||||||
-- START: column check
|
|
||||||
and (cursor_line0 ~= ext.start[1] or cursor_col0 >= ext.start[2])
|
|
||||||
-- STOP: line check
|
|
||||||
and cursor_line0 <= ext.stop[1]
|
|
||||||
-- STOP: column check
|
|
||||||
and (
|
|
||||||
cursor_line0 ~= ext.stop[1]
|
|
||||||
or (
|
|
||||||
mode == 'i'
|
|
||||||
-- In insert mode, the cursor is "thin", so <= to compensate:
|
|
||||||
and cursor_col0 <= ext.stop[2]
|
|
||||||
-- In normal mode, the cursor is "wide", so < to compensate:
|
|
||||||
or cursor_col0 < ext.stop[2]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else
|
else
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
@@ -617,8 +353,8 @@ function Renderer:get_tags_at(pos0, mode) -- {{{
|
|||||||
-- Sort the tags into smallest (inner) to largest (outer):
|
-- Sort the tags into smallest (inner) to largest (outer):
|
||||||
table.sort(
|
table.sort(
|
||||||
intersecting_extmarks,
|
intersecting_extmarks,
|
||||||
--- @param x1 u.renderer.RendererExtmark
|
--- @param x1 RendererExtmark
|
||||||
--- @param x2 u.renderer.RendererExtmark
|
--- @param x2 RendererExtmark
|
||||||
function(x1, x2)
|
function(x1, x2)
|
||||||
if
|
if
|
||||||
x1.start[1] == x2.start[1]
|
x1.start[1] == x2.start[1]
|
||||||
@@ -640,10 +376,10 @@ function Renderer:get_tags_at(pos0, mode) -- {{{
|
|||||||
-- created extmarks in self.curr.extmarks, which also has which tag each
|
-- created extmarks in self.curr.extmarks, which also has which tag each
|
||||||
-- extmark is associated with. Cross-reference with that list to get a list
|
-- extmark is associated with. Cross-reference with that list to get a list
|
||||||
-- of tags that we need to fire events for:
|
-- of tags that we need to fire events for:
|
||||||
--- @type { extmark: u.renderer.RendererExtmark; tag: u.renderer.Tag }[]
|
--- @type { extmark: RendererExtmark; tag: Tag }[]
|
||||||
local matching_tags = vim
|
local matching_tags = vim
|
||||||
.iter(intersecting_extmarks)
|
.iter(intersecting_extmarks)
|
||||||
--- @param ext u.renderer.RendererExtmark
|
--- @param ext RendererExtmark
|
||||||
:map(function(ext)
|
:map(function(ext)
|
||||||
for _, extmark_cache in ipairs(self.curr.extmarks) do
|
for _, extmark_cache in ipairs(self.curr.extmarks) do
|
||||||
if extmark_cache.id == ext.id then return { extmark = ext, tag = extmark_cache.tag } end
|
if extmark_cache.id == ext.id then return { extmark = ext, tag = extmark_cache.tag } end
|
||||||
@@ -652,25 +388,14 @@ function Renderer:get_tags_at(pos0, mode) -- {{{
|
|||||||
:totable()
|
:totable()
|
||||||
|
|
||||||
return matching_tags
|
return matching_tags
|
||||||
end -- }}}
|
end
|
||||||
|
|
||||||
--- @private
|
--------------------------------------------------------------------------------
|
||||||
--- @param tag_or_id string | u.renderer.Tag
|
-- TreeBuilder class
|
||||||
--- @return { start: [number, number]; stop: [number, number] } | nil
|
--------------------------------------------------------------------------------
|
||||||
function Renderer:get_tag_bounds(tag_or_id) -- {{{
|
|
||||||
for _, x in ipairs(self.curr.extmarks) do
|
|
||||||
local pos = { start = x.start, stop = x.stop }
|
|
||||||
local does_tag_match = type(tag_or_id) == 'string' and x.tag.attributes.id == tag_or_id
|
|
||||||
or x.tag == tag_or_id
|
|
||||||
if does_tag_match then return pos end
|
|
||||||
end
|
|
||||||
end -- }}}
|
|
||||||
|
|
||||||
-- }}}
|
--- @class TreeBuilder
|
||||||
|
--- @field private nodes Node[]
|
||||||
-- TreeBuilder {{{
|
|
||||||
--- @class u.renderer.TreeBuilder
|
|
||||||
--- @field private nodes u.renderer.Node[]
|
|
||||||
local TreeBuilder = {}
|
local TreeBuilder = {}
|
||||||
TreeBuilder.__index = TreeBuilder
|
TreeBuilder.__index = TreeBuilder
|
||||||
M.TreeBuilder = TreeBuilder
|
M.TreeBuilder = TreeBuilder
|
||||||
@@ -680,8 +405,8 @@ function TreeBuilder.new()
|
|||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param nodes u.renderer.Tree
|
--- @param nodes Tree
|
||||||
--- @return u.renderer.TreeBuilder
|
--- @return TreeBuilder
|
||||||
function TreeBuilder:put(nodes)
|
function TreeBuilder:put(nodes)
|
||||||
table.insert(self.nodes, nodes)
|
table.insert(self.nodes, nodes)
|
||||||
return self
|
return self
|
||||||
@@ -689,16 +414,16 @@ end
|
|||||||
|
|
||||||
--- @param name string
|
--- @param name string
|
||||||
--- @param attributes? table<string, any>
|
--- @param attributes? table<string, any>
|
||||||
--- @param children? u.renderer.Node | u.renderer.Node[]
|
--- @param children? Node | Node[]
|
||||||
--- @return u.renderer.TreeBuilder
|
--- @return TreeBuilder
|
||||||
function TreeBuilder:put_h(name, attributes, children)
|
function TreeBuilder:put_h(name, attributes, children)
|
||||||
local tag = M.h(name, attributes, children)
|
local tag = M.h(name, attributes, children)
|
||||||
table.insert(self.nodes, tag)
|
table.insert(self.nodes, tag)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param fn fun(tb: u.renderer.TreeBuilder): any
|
--- @param fn fun(TreeBuilder): any
|
||||||
--- @return u.renderer.TreeBuilder
|
--- @return TreeBuilder
|
||||||
function TreeBuilder:nest(fn)
|
function TreeBuilder:nest(fn)
|
||||||
local nested_writer = TreeBuilder.new()
|
local nested_writer = TreeBuilder.new()
|
||||||
fn(nested_writer)
|
fn(nested_writer)
|
||||||
@@ -706,132 +431,7 @@ function TreeBuilder:nest(fn)
|
|||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @generic T
|
--- @return Tree
|
||||||
--- @param arr T[]
|
|
||||||
--- @param f fun(tb: u.renderer.TreeBuilder, item: T, idx: number): any
|
|
||||||
function TreeBuilder:ipairs(arr, f)
|
|
||||||
return self:nest(function(tb)
|
|
||||||
for idx, item in ipairs(arr) do
|
|
||||||
f(tb, item, idx)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- @param tab table
|
|
||||||
--- @param f fun(tb: u.renderer.TreeBuilder, key: any, value: any): any
|
|
||||||
function TreeBuilder:pairs(tab, f)
|
|
||||||
return self:nest(function(tb)
|
|
||||||
for k, v in pairs(tab) do
|
|
||||||
f(tb, k, v)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- @return u.renderer.Tree
|
|
||||||
function TreeBuilder:tree() return self.nodes end
|
function TreeBuilder:tree() return self.nodes end
|
||||||
-- }}}
|
|
||||||
|
|
||||||
-- Levenshtein utility {{{
|
|
||||||
-- luacheck: ignore
|
|
||||||
--- @alias u.renderer.LevenshteinChange<T> ({ kind: 'add'; item: T; index: number; } | { kind: 'delete'; item: T; index: number; } | { kind: 'change'; from: T; to: T; index: number; })
|
|
||||||
--- @private
|
|
||||||
--- @generic T
|
|
||||||
--- @param x `T`[]
|
|
||||||
--- @param y T[]
|
|
||||||
--- @param cost? { of_delete?: fun(x: T): number; of_add?: fun(x: T): number; of_change?: fun(x: T, y: T): number; }
|
|
||||||
--- @return u.renderer.LevenshteinChange<T>[] The changes, from last (greatest index) to first (smallest index).
|
|
||||||
function H.levenshtein(x, y, cost)
|
|
||||||
-- At the moment, this whole `cost` plumbing is not used. Deletes have the
|
|
||||||
-- same cost as Adds or Changes. I can imagine a future, however, where
|
|
||||||
-- fudging with the costs of operations produces a more optimized change-set
|
|
||||||
-- that is tailored to working better with how NeoVim manipulates text. I've
|
|
||||||
-- done no further investigation in this area, however, so it's impossible to
|
|
||||||
-- tell if such tuning would produce real benefit. For now, I'm leaving this
|
|
||||||
-- in here even though it's not actively used. Hopefully having this
|
|
||||||
-- callback-based plumbing does not cause too much of a performance hit to
|
|
||||||
-- the renderer.
|
|
||||||
cost = cost or {}
|
|
||||||
local cost_of_delete_f = cost.of_delete or function() return 1 end
|
|
||||||
local cost_of_add_f = cost.of_add or function() return 1 end
|
|
||||||
local cost_of_change_f = cost.of_change or function() return 1 end
|
|
||||||
|
|
||||||
local m, n = #x, #y
|
|
||||||
-- Initialize the distance matrix
|
|
||||||
local dp = {}
|
|
||||||
for i = 0, m do
|
|
||||||
dp[i] = {}
|
|
||||||
for j = 0, n do
|
|
||||||
dp[i][j] = 0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Fill the base cases
|
|
||||||
for i = 0, m do
|
|
||||||
dp[i][0] = i
|
|
||||||
end
|
|
||||||
for j = 0, n do
|
|
||||||
dp[0][j] = j
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Compute the Levenshtein distance dynamically
|
|
||||||
for i = 1, m do
|
|
||||||
for j = 1, n do
|
|
||||||
if x[i] == y[j] then
|
|
||||||
dp[i][j] = dp[i - 1][j - 1] -- no cost if items are the same
|
|
||||||
else
|
|
||||||
local cost_delete = dp[i - 1][j] + cost_of_delete_f(x[i])
|
|
||||||
local cost_add = dp[i][j - 1] + cost_of_add_f(y[j])
|
|
||||||
local cost_change = dp[i - 1][j - 1] + cost_of_change_f(x[i], y[j])
|
|
||||||
dp[i][j] = math.min(cost_delete, cost_add, cost_change)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Backtrack to find the changes
|
|
||||||
local i = m
|
|
||||||
local j = n
|
|
||||||
--- @type u.renderer.LevenshteinChange[]
|
|
||||||
local changes = {}
|
|
||||||
|
|
||||||
while i > 0 or j > 0 do
|
|
||||||
local default_cost = dp[i][j]
|
|
||||||
local cost_of_change = (i > 0 and j > 0) and dp[i - 1][j - 1] or default_cost
|
|
||||||
local cost_of_add = j > 0 and dp[i][j - 1] or default_cost
|
|
||||||
local cost_of_delete = i > 0 and dp[i - 1][j] or default_cost
|
|
||||||
|
|
||||||
--- @param u number
|
|
||||||
--- @param v number
|
|
||||||
--- @param w number
|
|
||||||
local function is_first_min(u, v, w) return u <= v and u <= w end
|
|
||||||
|
|
||||||
if is_first_min(cost_of_change, cost_of_add, cost_of_delete) then
|
|
||||||
-- potential change
|
|
||||||
if x[i] ~= y[j] then
|
|
||||||
--- @type u.renderer.LevenshteinChange
|
|
||||||
local change = { kind = 'change', from = x[i], index = i, to = y[j] }
|
|
||||||
table.insert(changes, change)
|
|
||||||
end
|
|
||||||
i = i - 1
|
|
||||||
j = j - 1
|
|
||||||
elseif is_first_min(cost_of_add, cost_of_change, cost_of_delete) then
|
|
||||||
-- addition
|
|
||||||
--- @type u.renderer.LevenshteinChange
|
|
||||||
local change = { kind = 'add', item = y[j], index = i + 1 }
|
|
||||||
table.insert(changes, change)
|
|
||||||
j = j - 1
|
|
||||||
elseif is_first_min(cost_of_delete, cost_of_change, cost_of_add) then
|
|
||||||
-- deletion
|
|
||||||
--- @type u.renderer.LevenshteinChange
|
|
||||||
local change = { kind = 'delete', item = x[i], index = i }
|
|
||||||
table.insert(changes, change)
|
|
||||||
i = i - 1
|
|
||||||
else
|
|
||||||
error 'unreachable'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return changes
|
|
||||||
end
|
|
||||||
-- }}}
|
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -1,39 +1,61 @@
|
|||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
local IS_REPEATING = false
|
local function _normal(cmd) vim.cmd { cmd = 'normal', args = { cmd }, bang = true } end
|
||||||
--- @type function
|
|
||||||
local REPEAT_ACTION = nil
|
|
||||||
|
|
||||||
local function is_repeatable_last_mutator() return vim.b.changedtick <= (vim.b.my_changedtick or 0) end
|
M.native_repeat = function() _normal '.' end
|
||||||
|
M.native_undo = function() _normal 'u' end
|
||||||
|
|
||||||
--- @param f fun()
|
local function update_ts() vim.b.tt_changedtick = vim.b.changedtick end
|
||||||
function M.run_repeatable(f)
|
|
||||||
REPEAT_ACTION = f
|
---@param cmd? string|fun():unknown
|
||||||
REPEAT_ACTION()
|
function M.set(cmd)
|
||||||
vim.b.my_changedtick = vim.b.changedtick
|
update_ts()
|
||||||
|
if cmd ~= nil then vim.b.tt_repeatcmd = cmd end
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.is_repeating() return IS_REPEATING end
|
local function tt_was_last_repeatable()
|
||||||
|
local ts, tt_ts = vim.b.changedtick, vim.b.tt_changedtick
|
||||||
|
return tt_ts ~= nil and ts <= tt_ts
|
||||||
|
end
|
||||||
|
|
||||||
|
---@generic T
|
||||||
|
---@param cmd string|fun():T
|
||||||
|
---@return T
|
||||||
|
function M.run(cmd)
|
||||||
|
M.set(cmd)
|
||||||
|
local result = cmd()
|
||||||
|
update_ts()
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.do_repeat()
|
||||||
|
local tt_cmd = vim.b.tt_repeatcmd
|
||||||
|
if not tt_was_last_repeatable() or (type(tt_cmd) ~= 'function' and type(tt_cmd) ~= 'string') then
|
||||||
|
return M.native_repeat()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- execute the cached command:
|
||||||
|
local count = vim.api.nvim_get_vvar 'count1'
|
||||||
|
if type(tt_cmd) == 'string' then
|
||||||
|
_normal(count .. tt_cmd --[[@as string]])
|
||||||
|
else
|
||||||
|
local last_return
|
||||||
|
for _ = 1, count do
|
||||||
|
last_return = M.run(tt_cmd --[[@as fun():any]])
|
||||||
|
end
|
||||||
|
return last_return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.undo()
|
||||||
|
local tt_was_last_repeatable_before_undo = tt_was_last_repeatable()
|
||||||
|
M.native_undo()
|
||||||
|
if tt_was_last_repeatable_before_undo then update_ts() end
|
||||||
|
end
|
||||||
|
|
||||||
function M.setup()
|
function M.setup()
|
||||||
vim.keymap.set('n', '.', function()
|
vim.keymap.set('n', '.', M.do_repeat)
|
||||||
IS_REPEATING = true
|
vim.keymap.set('n', 'u', M.undo)
|
||||||
for _ = 1, vim.v.count1 do
|
|
||||||
if is_repeatable_last_mutator() and type(REPEAT_ACTION) == 'function' then
|
|
||||||
M.run_repeatable(REPEAT_ACTION)
|
|
||||||
else
|
|
||||||
vim.cmd { cmd = 'normal', args = { '.' }, bang = true }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
IS_REPEATING = false
|
|
||||||
end)
|
|
||||||
vim.keymap.set('n', 'u', function()
|
|
||||||
local was_repeatable_last_mutator = is_repeatable_last_mutator()
|
|
||||||
for _ = 1, vim.v.count1 do
|
|
||||||
vim.cmd { cmd = 'normal', args = { 'u' }, bang = true }
|
|
||||||
end
|
|
||||||
if was_repeatable_last_mutator then vim.b.my_changedtick = vim.b.changedtick end
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
90
lua/u/state.lua
Normal file
90
lua/u/state.lua
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
---@class State
|
||||||
|
---@field buf number
|
||||||
|
---@field registers table
|
||||||
|
---@field marks table
|
||||||
|
---@field positions table
|
||||||
|
---@field keymaps { mode: string; lhs: any, rhs: any, buffer?: number }[]
|
||||||
|
---@field global_options table<string, any>
|
||||||
|
---@field win_view vim.fn.winsaveview.ret|nil
|
||||||
|
local State = {}
|
||||||
|
|
||||||
|
---@param buf number
|
||||||
|
---@return State
|
||||||
|
function State.new(buf)
|
||||||
|
if buf == 0 then buf = vim.api.nvim_get_current_buf() end
|
||||||
|
local s = { buf = buf, registers = {}, marks = {}, positions = {}, keymaps = {}, global_options = {} }
|
||||||
|
setmetatable(s, { __index = State })
|
||||||
|
return s
|
||||||
|
end
|
||||||
|
|
||||||
|
---@generic T
|
||||||
|
---@param buf number
|
||||||
|
---@param f fun(s: State):T
|
||||||
|
---@return T
|
||||||
|
function State.run(buf, f)
|
||||||
|
local s = State.new(buf)
|
||||||
|
local ok, result = pcall(f, s)
|
||||||
|
s:restore()
|
||||||
|
if not ok then error(result) end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param buf number
|
||||||
|
---@param f fun(s: State, callback: fun(): any):any
|
||||||
|
---@param callback fun():any
|
||||||
|
function State.run_async(buf, f, callback)
|
||||||
|
local s = State.new(buf)
|
||||||
|
f(s, function()
|
||||||
|
s:restore()
|
||||||
|
callback()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function State:track_keymap(mode, lhs)
|
||||||
|
local old =
|
||||||
|
-- Look up the mapping in buffer-local maps:
|
||||||
|
vim.iter(vim.api.nvim_buf_get_keymap(self.buf, mode)):find(function(map) return map.lhs == lhs end)
|
||||||
|
-- Look up the mapping in global maps:
|
||||||
|
or vim.iter(vim.api.nvim_get_keymap(mode)):find(function(map) return map.lhs == lhs end)
|
||||||
|
|
||||||
|
-- Did we find a mapping?
|
||||||
|
if old == nil then return end
|
||||||
|
|
||||||
|
-- Track it:
|
||||||
|
table.insert(self.keymaps, { mode = mode, lhs = lhs, rhs = old.rhs or old.callback, buffer = old.buffer })
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param reg string
|
||||||
|
function State:track_register(reg) self.registers[reg] = vim.fn.getreg(reg) end
|
||||||
|
|
||||||
|
---@param mark string
|
||||||
|
function State:track_mark(mark) self.marks[mark] = vim.api.nvim_buf_get_mark(self.buf, mark) end
|
||||||
|
|
||||||
|
---@param pos string
|
||||||
|
function State:track_pos(pos) self.positions[pos] = vim.fn.getpos(pos) end
|
||||||
|
|
||||||
|
---@param nm string
|
||||||
|
function State:track_global_option(nm) self.global_options[nm] = vim.go[nm] end
|
||||||
|
|
||||||
|
function State:track_winview() self.win_view = vim.fn.winsaveview() end
|
||||||
|
|
||||||
|
function State:restore()
|
||||||
|
for reg, val in pairs(self.registers) do
|
||||||
|
vim.fn.setreg(reg, val)
|
||||||
|
end
|
||||||
|
for mark, val in pairs(self.marks) do
|
||||||
|
vim.api.nvim_buf_set_mark(self.buf, mark, val[1], val[2], {})
|
||||||
|
end
|
||||||
|
for pos, val in pairs(self.positions) do
|
||||||
|
vim.fn.setpos(pos, val)
|
||||||
|
end
|
||||||
|
for _, map in ipairs(self.keymaps) do
|
||||||
|
vim.keymap.set(map.mode, map.lhs, map.rhs, { buffer = map.buffer })
|
||||||
|
end
|
||||||
|
for nm, val in pairs(self.global_options) do
|
||||||
|
vim.go[nm] = val
|
||||||
|
end
|
||||||
|
if self.win_view ~= nil then vim.fn.winrestview(self.win_view) end
|
||||||
|
end
|
||||||
|
|
||||||
|
return State
|
||||||
@@ -6,19 +6,19 @@ M.debug = false
|
|||||||
-- class Signal
|
-- class Signal
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
--- @class u.Signal<T>
|
--- @class Signal
|
||||||
--- @field name? string
|
--- @field name? string
|
||||||
--- @field private changing boolean
|
--- @field private changing boolean
|
||||||
--- @field private value T
|
--- @field private value any
|
||||||
--- @field private subscribers table<function, boolean>
|
--- @field private subscribers table<function, boolean>
|
||||||
--- @field private on_dispose_callbacks function[]
|
--- @field private on_dispose_callbacks function[]
|
||||||
local Signal = {}
|
local Signal = {}
|
||||||
M.Signal = Signal
|
M.Signal = Signal
|
||||||
Signal.__index = Signal
|
Signal.__index = Signal
|
||||||
|
|
||||||
--- @param value `T`
|
--- @param value any
|
||||||
--- @param name? string
|
--- @param name? string
|
||||||
--- @return u.Signal<T>
|
--- @return Signal
|
||||||
function Signal:new(value, name)
|
function Signal:new(value, name)
|
||||||
local obj = setmetatable({
|
local obj = setmetatable({
|
||||||
name = name,
|
name = name,
|
||||||
@@ -30,17 +30,14 @@ function Signal:new(value, name)
|
|||||||
return obj
|
return obj
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param value T
|
--- @param value any
|
||||||
function Signal:set(value)
|
function Signal:set(value)
|
||||||
self.value = value
|
self.value = value
|
||||||
|
|
||||||
-- 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(
|
vim.notify('circular dependency detected' .. (self.name and (' in ' .. self.name) or ''), vim.log.levels.WARN)
|
||||||
'circular dependency detected' .. (self.name and (' in ' .. self.name) or ''),
|
|
||||||
vim.log.levels.WARN
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -67,12 +64,11 @@ function Signal:set(value)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param value T
|
|
||||||
function Signal:schedule_set(value)
|
function Signal:schedule_set(value)
|
||||||
vim.schedule(function() self:set(value) end)
|
vim.schedule(function() self:set(value) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @return T
|
--- @return any
|
||||||
function Signal:get()
|
function Signal:get()
|
||||||
local ctx = M.ExecutionContext.current()
|
local ctx = M.ExecutionContext.current()
|
||||||
if ctx then ctx:track(self) end
|
if ctx then ctx:track(self) end
|
||||||
@@ -86,8 +82,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 Signal --<U>
|
||||||
function Signal:map(fn)
|
function Signal:map(fn)
|
||||||
local mapped_signal = M.create_memo(function()
|
local mapped_signal = M.create_memo(function()
|
||||||
local value = self:get()
|
local value = self:get()
|
||||||
@@ -96,13 +92,13 @@ function Signal:map(fn)
|
|||||||
return mapped_signal
|
return mapped_signal
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @return u.Signal<T>
|
--- @return Signal
|
||||||
function Signal:clone()
|
function Signal:clone()
|
||||||
return self:map(function(x) return x end)
|
return self:map(function(x) return x end)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param fn fun(value: T): boolean
|
--- @param fn fun(value: T): boolean
|
||||||
--- @return u.Signal<T>
|
--- @return Signal -- <T>
|
||||||
function Signal:filter(fn)
|
function Signal:filter(fn)
|
||||||
local filtered_signal = M.create_signal(nil, self.name and self.name .. ':filtered' or nil)
|
local filtered_signal = M.create_signal(nil, self.name and self.name .. ':filtered' or nil)
|
||||||
local unsubscribe_from_self = self:subscribe(function(value)
|
local unsubscribe_from_self = self:subscribe(function(value)
|
||||||
@@ -113,10 +109,10 @@ function Signal:filter(fn)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- @param ms number
|
--- @param ms number
|
||||||
--- @return u.Signal<T>
|
--- @return Signal -- <T>
|
||||||
function Signal:debounce(ms)
|
function Signal:debounce(ms)
|
||||||
local function set_timeout(timeout, callback)
|
local function set_timeout(timeout, callback)
|
||||||
local timer = assert((vim.uv or vim.loop).new_timer(), 'could not create new timer')
|
local timer = (vim.uv or vim.loop).new_timer()
|
||||||
timer:start(timeout, 0, function()
|
timer:start(timeout, 0, function()
|
||||||
timer:stop()
|
timer:stop()
|
||||||
timer:close()
|
timer:close()
|
||||||
@@ -127,15 +123,15 @@ 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
|
--- @type {
|
||||||
--- @type { queued: { value: T, ts: number }[], timer?: uv.uv_timer_t }
|
-- queued: { value: T, ts: number }[]
|
||||||
|
-- timer?: uv_timer_t
|
||||||
|
-- }
|
||||||
local state = { queued = {}, timer = nil }
|
local state = { queued = {}, timer = nil }
|
||||||
local function clear_timeout()
|
local function clear_timeout()
|
||||||
if state.timer == nil then return end
|
if state.timer == nil then return end
|
||||||
pcall(function()
|
pcall(function()
|
||||||
--- @diagnostic disable-next-line: undefined-field
|
|
||||||
state.timer:stop()
|
state.timer:stop()
|
||||||
--- @diagnostic disable-next-line: undefined-field
|
|
||||||
state.timer:close()
|
state.timer:close()
|
||||||
end)
|
end)
|
||||||
state.timer = nil
|
state.timer = nil
|
||||||
@@ -147,10 +143,7 @@ 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
|
local older_than_ms = vim.iter(state.queued):filter(function(item) return now_ms - item.ts > ms end):totable()
|
||||||
.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)
|
||||||
@@ -203,17 +196,16 @@ end
|
|||||||
-- class ExecutionContext
|
-- class ExecutionContext
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
--- @type u.ExecutionContext|nil
|
CURRENT_CONTEXT = nil
|
||||||
local CURRENT_CONTEXT = nil
|
|
||||||
|
|
||||||
--- @class u.ExecutionContext
|
--- @class ExecutionContext
|
||||||
--- @field signals table<u.Signal, boolean>
|
--- @field signals table<Signal, boolean>
|
||||||
local ExecutionContext = {}
|
local ExecutionContext = {}
|
||||||
M.ExecutionContext = ExecutionContext
|
M.ExecutionContext = ExecutionContext
|
||||||
ExecutionContext.__index = ExecutionContext
|
ExecutionContext.__index = ExecutionContext
|
||||||
|
|
||||||
--- @return u.ExecutionContext
|
--- @return ExecutionContext
|
||||||
function ExecutionContext.new()
|
function ExecutionContext:new()
|
||||||
return setmetatable({
|
return setmetatable({
|
||||||
signals = {},
|
signals = {},
|
||||||
subscribers = {},
|
subscribers = {},
|
||||||
@@ -223,8 +215,8 @@ end
|
|||||||
function ExecutionContext.current() return CURRENT_CONTEXT end
|
function ExecutionContext.current() return CURRENT_CONTEXT end
|
||||||
|
|
||||||
--- @param fn function
|
--- @param fn function
|
||||||
--- @param ctx u.ExecutionContext
|
--- @param ctx 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
|
||||||
@@ -264,18 +256,16 @@ end
|
|||||||
-- Helpers
|
-- Helpers
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
--- @generic T
|
--- @param value any
|
||||||
--- @param value `T`
|
|
||||||
--- @param name? string
|
--- @param name? string
|
||||||
--- @return u.Signal<T>
|
--- @return Signal
|
||||||
function M.create_signal(value, name) return Signal:new(value, name) end
|
function M.create_signal(value, name) return Signal:new(value, name) end
|
||||||
|
|
||||||
--- @generic T
|
--- @param fn function
|
||||||
--- @param fn fun(): `T`
|
|
||||||
--- @param name? string
|
--- @param name? string
|
||||||
--- @return u.Signal
|
--- @return Signal
|
||||||
function M.create_memo(fn, name)
|
function M.create_memo(fn, name)
|
||||||
--- @type u.Signal<T> | nil
|
--- @type Signal
|
||||||
local result
|
local result
|
||||||
local unsubscribe = M.create_effect(function()
|
local unsubscribe = M.create_effect(function()
|
||||||
local value = fn()
|
local value = fn()
|
||||||
@@ -286,15 +276,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)
|
||||||
assert(result):on_dispose(unsubscribe)
|
result:on_dispose(unsubscribe)
|
||||||
return assert(result)
|
return 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
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
local Range = require 'u.range'
|
|
||||||
|
|
||||||
local M = {}
|
|
||||||
|
|
||||||
local ESC = vim.api.nvim_replace_termcodes('<Esc>', true, false, true)
|
|
||||||
|
|
||||||
--- @param key_seq string
|
|
||||||
--- @param fn fun(key_seq: string):u.Range|nil
|
|
||||||
--- @param opts? { buffer: number|nil }
|
|
||||||
function M.define(key_seq, fn, opts)
|
|
||||||
if opts ~= nil and opts.buffer == 0 then opts.buffer = vim.api.nvim_get_current_buf() end
|
|
||||||
|
|
||||||
local function handle_visual()
|
|
||||||
local range = fn(key_seq)
|
|
||||||
if range == nil or range:is_empty() then
|
|
||||||
vim.cmd.normal(ESC)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
range:set_visual_selection()
|
|
||||||
end
|
|
||||||
vim.keymap.set({ 'x' }, key_seq, handle_visual, opts and { buffer = opts.buffer } or nil)
|
|
||||||
|
|
||||||
local function handle_normal()
|
|
||||||
local range = fn(key_seq)
|
|
||||||
if range == nil then return end
|
|
||||||
|
|
||||||
if not range:is_empty() then
|
|
||||||
range:set_visual_selection()
|
|
||||||
else
|
|
||||||
local original_eventignore = vim.go.eventignore
|
|
||||||
vim.go.eventignore = 'all'
|
|
||||||
|
|
||||||
-- insert a single space, so we can select it:
|
|
||||||
local p = range.start
|
|
||||||
p:insert_before ' '
|
|
||||||
vim.go.eventignore = original_eventignore
|
|
||||||
|
|
||||||
-- select the space:
|
|
||||||
Range.new(p, p, 'v'):set_visual_selection()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
vim.keymap.set({ 'o' }, key_seq, handle_normal, opts and { buffer = opts.buffer } or nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
return M
|
|
||||||
326
lua/u/utils.lua
326
lua/u/utils.lua
@@ -4,63 +4,14 @@ local M = {}
|
|||||||
-- Types
|
-- Types
|
||||||
--
|
--
|
||||||
|
|
||||||
--- @class u.utils.QfItem
|
---@alias QfItem { col: number, filename: string, kind: string, lnum: number, text: string }
|
||||||
--- @field col number
|
---@alias KeyMaps table<string, fun(): any | string> }
|
||||||
--- @field filename string
|
---@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: Range|nil }
|
||||||
--- @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` The value to debug print
|
--- @param x `T`
|
||||||
--- @param message? string Optional message to print alongside the value
|
--- @param message? string
|
||||||
--- @return T The original value, unchanged
|
--- @return T
|
||||||
---
|
|
||||||
--- @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
|
||||||
@@ -69,37 +20,21 @@ function M.dbg(x, message)
|
|||||||
return x
|
return x
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Creates a user command with enhanced argument processing.
|
--- A utility for creating user commands that also pre-computes useful information
|
||||||
--- Automatically computes range information and attaches it as `args.info`.
|
--- and attaches it to the arguments.
|
||||||
---
|
---
|
||||||
--- @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
|
||||||
--- -- Create a command that works with visual selections:
|
--- -- Example:
|
||||||
--- utils.ucmd('MyCmd', function(args)
|
--- 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 selection as an array of lines:
|
--- -- or get the vtext 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 })
|
|
||||||
--- ```
|
--- ```
|
||||||
-- luacheck: ignore
|
---@param name string
|
||||||
|
---@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'
|
||||||
|
|
||||||
@@ -111,81 +46,170 @@ 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 {} --[[@as any]])
|
vim.api.nvim_create_user_command(name, cmd2, opts or {})
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Creates command arguments for delegating from one command to another.
|
---@param key_seq string
|
||||||
--- Preserves all relevant context (range, modifiers, bang, etc.) when
|
---@param fn fun(key_seq: string):Range|Pos|nil
|
||||||
--- implementing a derived command in terms of a base command.
|
---@param opts? { buffer: number|nil }
|
||||||
---
|
function M.define_text_object(key_seq, fn, opts)
|
||||||
--- @param current_args vim.api.keyset.create_user_command.command_args|u.utils.RawCmdArgs The arguments from the current command
|
local Range = require 'u.range'
|
||||||
--- @return vim.api.keyset.cmd Arguments suitable for vim.cmd() calls
|
local Pos = require 'u.pos'
|
||||||
---
|
|
||||||
--- @usage
|
if opts ~= nil and opts.buffer == 0 then opts.buffer = vim.api.nvim_get_current_buf() end
|
||||||
--- ```lua
|
|
||||||
--- -- Implement :MyEdit in terms of :edit, preserving all context:
|
local function handle_visual()
|
||||||
--- utils.ucmd('MyEdit', function(args)
|
local range_or_pos = fn(key_seq)
|
||||||
--- local delegated_args = utils.create_delegated_cmd_args(args)
|
if range_or_pos == nil then return end
|
||||||
--- -- Add custom logic here...
|
if Range.is(range_or_pos) and range_or_pos:is_empty() then range_or_pos = range_or_pos.start end
|
||||||
--- vim.cmd.edit(delegated_args)
|
|
||||||
--- end, { nargs = '*', range = true, bang = true })
|
if Range.is(range_or_pos) then
|
||||||
---
|
local range = range_or_pos --[[@as Range]]
|
||||||
--- -- Implement :MySubstitute that delegates to :substitute:
|
range:set_visual_selection()
|
||||||
--- utils.ucmd('MySubstitute', function(args)
|
else
|
||||||
--- -- Pre-process arguments
|
vim.cmd { cmd = 'normal', args = { '<Esc>' }, bang = true }
|
||||||
--- local pattern = preprocess_pattern(args.fargs[1])
|
end
|
||||||
---
|
end
|
||||||
--- local delegated_args = utils.create_delegated_cmd_args(args)
|
vim.keymap.set({ 'x' }, key_seq, handle_visual, opts and { buffer = opts.buffer } or nil)
|
||||||
--- delegated_args.args = { pattern, args.fargs[2] }
|
|
||||||
---
|
local function handle_normal()
|
||||||
--- vim.cmd.substitute(delegated_args)
|
local State = require 'u.state'
|
||||||
--- end, { nargs = 2, range = true, bang = true })
|
|
||||||
--- ```
|
-- enter visual mode:
|
||||||
function M.create_delegated_cmd_args(current_args)
|
vim.cmd { cmd = 'normal', args = { 'v' }, bang = true }
|
||||||
--- @type vim.api.keyset.cmd
|
|
||||||
local args = {
|
local range_or_pos = fn(key_seq)
|
||||||
range = current_args.range == 1 and { current_args.line1 }
|
if range_or_pos == nil then return end
|
||||||
or current_args.range == 2 and { current_args.line1, current_args.line2 }
|
if Range.is(range_or_pos) and range_or_pos:is_empty() then range_or_pos = range_or_pos.start end
|
||||||
or nil,
|
|
||||||
count = (current_args.count ~= -1 and current_args.range == 0) and current_args.count or nil,
|
if Range.is(range_or_pos) then
|
||||||
reg = current_args.reg ~= '' and current_args.reg or nil,
|
range_or_pos:set_visual_selection()
|
||||||
bang = current_args.bang or nil,
|
elseif Pos.is(range_or_pos) then
|
||||||
args = #current_args.fargs > 0 and current_args.fargs or nil,
|
local p = range_or_pos --[[@as Pos]]
|
||||||
mods = current_args.smods,
|
State.run(0, function(s)
|
||||||
}
|
s:track_global_option 'eventignore'
|
||||||
return args
|
vim.go.eventignore = 'all'
|
||||||
|
|
||||||
|
-- insert a single space, so we can select it:
|
||||||
|
vim.api.nvim_buf_set_text(0, p.lnum, p.col, p.lnum, p.col, { ' ' })
|
||||||
|
-- select the space:
|
||||||
|
Range.new(p, p, 'v'):set_visual_selection()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
vim.keymap.set({ 'o' }, key_seq, handle_normal, opts and { buffer = opts.buffer } or nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@type fun(): nil|(fun():any)
|
||||||
|
local __U__RepeatableOpFunc_rhs = nil
|
||||||
|
|
||||||
|
--- This is the global utility function used for operatorfunc
|
||||||
|
--- in repeatablemap
|
||||||
|
---@type nil|fun(range: Range): fun():any|nil
|
||||||
|
-- selene: allow(unused_variable)
|
||||||
|
function __U__RepeatableOpFunc()
|
||||||
|
if __U__RepeatableOpFunc_rhs ~= nil then __U__RepeatableOpFunc_rhs() end
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.repeatablemap(mode, lhs, rhs, opts)
|
||||||
|
vim.keymap.set(mode, lhs, function()
|
||||||
|
__U__RepeatableOpFunc_rhs = rhs
|
||||||
|
vim.o.operatorfunc = 'v:lua.__U__RepeatableOpFunc'
|
||||||
|
return 'g@ '
|
||||||
|
end, vim.tbl_extend('force', opts or {}, { expr = true }))
|
||||||
end
|
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
|
||||||
|
|
||||||
|
--- @alias LevenshteinChange<T> ({ kind: 'add'; item: T; index: number; } | { kind: 'delete'; item: T; index: number; } | { kind: 'change'; from: T; to: T; index: number; })
|
||||||
|
--- @private
|
||||||
|
--- @generic T
|
||||||
|
--- @param x `T`[]
|
||||||
|
--- @param y T[]
|
||||||
|
--- @param cost? { of_delete?: fun(x: T): number; of_add?: fun(x: T): number; of_change?: fun(x: T, y: T): number; }
|
||||||
|
--- @return LevenshteinChange<T>[]
|
||||||
|
function M.levenshtein(x, y, cost)
|
||||||
|
cost = cost or {}
|
||||||
|
local cost_of_delete_f = cost.of_delete or function() return 1 end
|
||||||
|
local cost_of_add_f = cost.of_add or function() return 1 end
|
||||||
|
local cost_of_change_f = cost.of_change or function() return 1 end
|
||||||
|
|
||||||
|
local m, n = #x, #y
|
||||||
|
-- Initialize the distance matrix
|
||||||
|
local dp = {}
|
||||||
|
for i = 0, m do
|
||||||
|
dp[i] = {}
|
||||||
|
for j = 0, n do
|
||||||
|
dp[i][j] = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Fill the base cases
|
||||||
|
for i = 0, m do
|
||||||
|
dp[i][0] = i
|
||||||
|
end
|
||||||
|
for j = 0, n do
|
||||||
|
dp[0][j] = j
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Compute the Levenshtein distance dynamically
|
||||||
|
for i = 1, m do
|
||||||
|
for j = 1, n do
|
||||||
|
if x[i] == y[j] then
|
||||||
|
dp[i][j] = dp[i - 1][j - 1] -- no cost if items are the same
|
||||||
|
else
|
||||||
|
local costDelete = dp[i - 1][j] + cost_of_delete_f(x[i])
|
||||||
|
local costAdd = dp[i][j - 1] + cost_of_add_f(y[j])
|
||||||
|
local costChange = dp[i - 1][j - 1] + cost_of_change_f(x[i], y[j])
|
||||||
|
dp[i][j] = math.min(costDelete, costAdd, costChange)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Backtrack to find the changes
|
||||||
|
local i = m
|
||||||
|
local j = n
|
||||||
|
--- @type LevenshteinChange[]
|
||||||
|
local changes = {}
|
||||||
|
|
||||||
|
while i > 0 or j > 0 do
|
||||||
|
local default_cost = dp[i][j]
|
||||||
|
local cost_of_change = (i > 0 and j > 0) and dp[i - 1][j - 1] or default_cost
|
||||||
|
local cost_of_add = j > 0 and dp[i][j - 1] or default_cost
|
||||||
|
local cost_of_delete = i > 0 and dp[i - 1][j] or default_cost
|
||||||
|
|
||||||
|
--- @param u number
|
||||||
|
--- @param v number
|
||||||
|
--- @param w number
|
||||||
|
local function is_first_min(u, v, w) return u <= v and u <= w end
|
||||||
|
|
||||||
|
if is_first_min(cost_of_change, cost_of_add, cost_of_delete) then
|
||||||
|
-- potential change
|
||||||
|
if x[i] ~= y[j] then
|
||||||
|
--- @type LevenshteinChange
|
||||||
|
local change = { kind = 'change', from = x[i], index = i, to = y[j] }
|
||||||
|
table.insert(changes, change)
|
||||||
|
end
|
||||||
|
i = i - 1
|
||||||
|
j = j - 1
|
||||||
|
elseif is_first_min(cost_of_add, cost_of_change, cost_of_delete) then
|
||||||
|
-- addition
|
||||||
|
--- @type LevenshteinChange
|
||||||
|
local change = { kind = 'add', item = y[j], index = i + 1 }
|
||||||
|
table.insert(changes, change)
|
||||||
|
j = j - 1
|
||||||
|
elseif is_first_min(cost_of_delete, cost_of_change, cost_of_add) then
|
||||||
|
-- deletion
|
||||||
|
--- @type LevenshteinChange
|
||||||
|
local change = { kind = 'delete', item = x[i], index = i }
|
||||||
|
table.insert(changes, change)
|
||||||
|
i = i - 1
|
||||||
|
else
|
||||||
|
error 'unreachable'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return changes
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
4
selene.toml
Normal file
4
selene.toml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
std = "vim"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
multiple_statements = "allow"
|
||||||
23
shell.nix
23
shell.nix
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
pkgs ?
|
|
||||||
import
|
|
||||||
# nixos-25.05 (neovim@0.11.2):
|
|
||||||
(fetchTarball {
|
|
||||||
url = "https://github.com/nixos/nixpkgs/archive/1c1c9b3f5ec0421eaa0f22746295466ee6a8d48f.tar.gz";
|
|
||||||
sha256 = "0szvq1swpzyjmyyw929ngxy1khdnd9ba96qds2bm6l6kg4iq3cq0";
|
|
||||||
})
|
|
||||||
{ },
|
|
||||||
}:
|
|
||||||
pkgs.mkShell {
|
|
||||||
packages = [
|
|
||||||
pkgs.git
|
|
||||||
pkgs.gnumake
|
|
||||||
pkgs.lua-language-server
|
|
||||||
pkgs.lua51Packages.busted
|
|
||||||
pkgs.lua51Packages.luacov
|
|
||||||
pkgs.lua51Packages.luarocks
|
|
||||||
pkgs.neovim
|
|
||||||
pkgs.stylua
|
|
||||||
pkgs.watchexec
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ describe('Buffer', function()
|
|||||||
withbuf({}, function()
|
withbuf({}, function()
|
||||||
local buf = Buffer.from_nr()
|
local buf = Buffer.from_nr()
|
||||||
buf:all():replace 'bleh'
|
buf:all():replace 'bleh'
|
||||||
local actual_lines = vim.api.nvim_buf_get_lines(buf.bufnr, 0, -1, false)
|
local actual_lines = vim.api.nvim_buf_get_lines(buf.buf, 0, -1, false)
|
||||||
assert.are.same({ 'bleh' }, actual_lines)
|
assert.are.same({ 'bleh' }, actual_lines)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
@@ -18,8 +18,8 @@ describe('Buffer', function()
|
|||||||
'three',
|
'three',
|
||||||
}, function()
|
}, function()
|
||||||
local buf = Buffer.from_nr()
|
local buf = Buffer.from_nr()
|
||||||
buf:lines(2, -2):replace 'too'
|
buf:lines(1, -2):replace 'too'
|
||||||
local actual_lines = vim.api.nvim_buf_get_lines(buf.bufnr, 0, -1, false)
|
local actual_lines = vim.api.nvim_buf_get_lines(buf.buf, 0, -1, false)
|
||||||
assert.are.same({
|
assert.are.same({
|
||||||
'one',
|
'one',
|
||||||
'too',
|
'too',
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ local withbuf = loadfile './spec/withbuf.lua'()
|
|||||||
describe('Pos', function()
|
describe('Pos', function()
|
||||||
it('get a char from a given position', function()
|
it('get a char from a given position', function()
|
||||||
withbuf({ 'asdf', 'bleh', 'a', '', 'goo' }, function()
|
withbuf({ 'asdf', 'bleh', 'a', '', 'goo' }, function()
|
||||||
assert.are.same('a', Pos.new(nil, 1, 1):char())
|
assert.are.same('a', Pos.new(nil, 0, 0):char())
|
||||||
assert.are.same('d', Pos.new(nil, 1, 3):char())
|
assert.are.same('d', Pos.new(nil, 0, 2):char())
|
||||||
assert.are.same('f', Pos.new(nil, 1, 4):char())
|
assert.are.same('f', Pos.new(nil, 0, 3):char())
|
||||||
assert.are.same('a', Pos.new(nil, 3, 1):char())
|
assert.are.same('a', Pos.new(nil, 2, 0):char())
|
||||||
assert.are.same('', Pos.new(nil, 4, 1):char())
|
assert.are.same('', Pos.new(nil, 3, 0):char())
|
||||||
assert.are.same('o', Pos.new(nil, 5, 3):char())
|
assert.are.same('o', Pos.new(nil, 4, 2):char())
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -23,47 +23,47 @@ describe('Pos', function()
|
|||||||
it('get the next position', function()
|
it('get the next position', function()
|
||||||
withbuf({ 'asdf', 'bleh', 'a', '', 'goo' }, function()
|
withbuf({ 'asdf', 'bleh', 'a', '', 'goo' }, function()
|
||||||
-- line 1: a => s
|
-- line 1: a => s
|
||||||
assert.are.same(Pos.new(nil, 1, 2), Pos.new(nil, 1, 1):next())
|
assert.are.same(Pos.new(nil, 0, 1), Pos.new(nil, 0, 0):next())
|
||||||
-- line 1: d => f
|
-- line 1: d => f
|
||||||
assert.are.same(Pos.new(nil, 1, 4), Pos.new(nil, 1, 3):next())
|
assert.are.same(Pos.new(nil, 0, 3), Pos.new(nil, 0, 2):next())
|
||||||
-- line 1 => 2
|
-- line 1 => 2
|
||||||
assert.are.same(Pos.new(nil, 2, 1), Pos.new(nil, 1, 4):next())
|
assert.are.same(Pos.new(nil, 1, 0), Pos.new(nil, 0, 3):next())
|
||||||
-- line 3 => 4
|
-- line 3 => 4
|
||||||
assert.are.same(Pos.new(nil, 4, 1), Pos.new(nil, 3, 1):next())
|
assert.are.same(Pos.new(nil, 3, 0), Pos.new(nil, 2, 0):next())
|
||||||
-- line 4 => 5
|
-- line 4 => 5
|
||||||
assert.are.same(Pos.new(nil, 5, 1), Pos.new(nil, 4, 1):next())
|
assert.are.same(Pos.new(nil, 4, 0), Pos.new(nil, 3, 0):next())
|
||||||
-- end returns nil
|
-- end returns nil
|
||||||
assert.are.same(nil, Pos.new(nil, 5, 3):next())
|
assert.are.same(nil, Pos.new(nil, 4, 2):next())
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('get the previous position', function()
|
it('get the previous position', function()
|
||||||
withbuf({ 'asdf', 'bleh', 'a', '', 'goo' }, function()
|
withbuf({ 'asdf', 'bleh', 'a', '', 'goo' }, function()
|
||||||
-- line 1: s => a
|
-- line 1: s => a
|
||||||
assert.are.same(Pos.new(nil, 1, 1), Pos.new(nil, 1, 2):next(-1))
|
assert.are.same(Pos.new(nil, 0, 0), Pos.new(nil, 0, 1):next(-1))
|
||||||
-- line 1: f => d
|
-- line 1: f => d
|
||||||
assert.are.same(Pos.new(nil, 1, 3), Pos.new(nil, 1, 4):next(-1))
|
assert.are.same(Pos.new(nil, 0, 2), Pos.new(nil, 0, 3):next(-1))
|
||||||
-- line 2 => 1
|
-- line 2 => 1
|
||||||
assert.are.same(Pos.new(nil, 1, 4), Pos.new(nil, 2, 1):next(-1))
|
assert.are.same(Pos.new(nil, 0, 3), Pos.new(nil, 1, 0):next(-1))
|
||||||
-- line 4 => 3
|
-- line 4 => 3
|
||||||
assert.are.same(Pos.new(nil, 3, 1), Pos.new(nil, 4, 1):next(-1))
|
assert.are.same(Pos.new(nil, 2, 0), Pos.new(nil, 3, 0):next(-1))
|
||||||
-- line 5 => 4
|
-- line 5 => 4
|
||||||
assert.are.same(Pos.new(nil, 4, 1), Pos.new(nil, 5, 1):next(-1))
|
assert.are.same(Pos.new(nil, 3, 0), Pos.new(nil, 4, 0):next(-1))
|
||||||
-- beginning returns nil
|
-- beginning returns nil
|
||||||
assert.are.same(nil, Pos.new(nil, 1, 1):next(-1))
|
assert.are.same(nil, Pos.new(nil, 0, 0):next(-1))
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('find matching brackets', function()
|
it('find matching brackets', function()
|
||||||
withbuf({ 'asdf ({} def <[{}]>) ;lkj' }, function()
|
withbuf({ 'asdf ({} def <[{}]>) ;lkj' }, function()
|
||||||
-- outer parens are matched:
|
-- outer parens are matched:
|
||||||
assert.are.same(Pos.new(nil, 1, 20), Pos.new(nil, 1, 6):find_match())
|
assert.are.same(Pos.new(nil, 0, 19), Pos.new(nil, 0, 5):find_match())
|
||||||
-- outer parens are matched (backward):
|
-- outer parens are matched (backward):
|
||||||
assert.are.same(Pos.new(nil, 1, 6), Pos.new(nil, 1, 20):find_match())
|
assert.are.same(Pos.new(nil, 0, 5), Pos.new(nil, 0, 19):find_match())
|
||||||
-- no potential match returns nil
|
-- no potential match returns nil
|
||||||
assert.are.same(nil, Pos.new(nil, 1, 1):find_match())
|
assert.are.same(nil, Pos.new(nil, 0, 0):find_match())
|
||||||
-- watchdog expires before an otherwise valid match is found:
|
-- watchdog expires before an otherwise valid match is found:
|
||||||
assert.are.same(nil, Pos.new(nil, 1, 6):find_match(2))
|
assert.are.same(nil, Pos.new(nil, 0, 5):find_match(2))
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
local Pos = require 'u.pos'
|
|
||||||
local Range = require 'u.range'
|
local Range = require 'u.range'
|
||||||
|
local Pos = require 'u.pos'
|
||||||
local withbuf = loadfile './spec/withbuf.lua'()
|
local withbuf = loadfile './spec/withbuf.lua'()
|
||||||
|
|
||||||
describe('Range', function()
|
describe('Range', function()
|
||||||
@@ -28,7 +28,7 @@ describe('Range', function()
|
|||||||
|
|
||||||
it('get from positions: v in single line', function()
|
it('get from positions: v in single line', function()
|
||||||
withbuf({ 'line one', 'and line two' }, function()
|
withbuf({ 'line one', 'and line two' }, function()
|
||||||
local range = Range.new(Pos.new(nil, 1, 2), Pos.new(nil, 1, 4), 'v')
|
local range = Range.new(Pos.new(nil, 0, 1), Pos.new(nil, 0, 3), 'v')
|
||||||
local lines = range:lines()
|
local lines = range:lines()
|
||||||
assert.are.same({ 'ine' }, lines)
|
assert.are.same({ 'ine' }, lines)
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ describe('Range', function()
|
|||||||
|
|
||||||
it('get from positions: v across multiple lines', function()
|
it('get from positions: v across multiple lines', function()
|
||||||
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
||||||
local range = Range.new(Pos.new(nil, 2, 5), Pos.new(nil, 3, 5), 'v')
|
local range = Range.new(Pos.new(nil, 1, 4), Pos.new(nil, 2, 4), 'v')
|
||||||
local lines = range:lines()
|
local lines = range:lines()
|
||||||
assert.are.same({ 'quick brown fox', 'jumps' }, lines)
|
assert.are.same({ 'quick brown fox', 'jumps' }, lines)
|
||||||
end)
|
end)
|
||||||
@@ -47,7 +47,7 @@ describe('Range', function()
|
|||||||
|
|
||||||
it('get from positions: V', function()
|
it('get from positions: V', function()
|
||||||
withbuf({ 'line one', 'and line two' }, function()
|
withbuf({ 'line one', 'and line two' }, function()
|
||||||
local range = Range.new(Pos.new(nil, 1, 1), Pos.new(nil, 1, Pos.MAX_COL), 'V')
|
local range = Range.new(Pos.new(nil, 0, 0), Pos.new(nil, 0, Pos.MAX_COL), 'V')
|
||||||
local lines = range:lines()
|
local lines = range:lines()
|
||||||
assert.are.same({ 'line one' }, lines)
|
assert.are.same({ 'line one' }, lines)
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ describe('Range', function()
|
|||||||
|
|
||||||
it('get from positions: V across multiple lines', function()
|
it('get from positions: V across multiple lines', function()
|
||||||
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
||||||
local range = Range.new(Pos.new(nil, 2, 1), Pos.new(nil, 3, Pos.MAX_COL), 'V')
|
local range = Range.new(Pos.new(nil, 1, 0), Pos.new(nil, 2, Pos.MAX_COL), 'V')
|
||||||
local lines = range:lines()
|
local lines = range:lines()
|
||||||
assert.are.same({ 'the quick brown fox', 'jumps over a lazy dog' }, lines)
|
assert.are.same({ 'the quick brown fox', 'jumps over a lazy dog' }, lines)
|
||||||
end)
|
end)
|
||||||
@@ -66,7 +66,7 @@ describe('Range', function()
|
|||||||
|
|
||||||
it('get from line', function()
|
it('get from line', function()
|
||||||
withbuf({ 'line one', 'and line two' }, function()
|
withbuf({ 'line one', 'and line two' }, function()
|
||||||
local range = Range.from_line(nil, 1)
|
local range = Range.from_line(nil, 0)
|
||||||
local lines = range:lines()
|
local lines = range:lines()
|
||||||
assert.are.same({ 'line one' }, lines)
|
assert.are.same({ 'line one' }, lines)
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ describe('Range', function()
|
|||||||
|
|
||||||
it('get from lines', function()
|
it('get from lines', function()
|
||||||
withbuf({ 'line one', 'and line two', 'and line 3' }, function()
|
withbuf({ 'line one', 'and line two', 'and line 3' }, function()
|
||||||
local range = Range.from_lines(nil, 1, 2)
|
local range = Range.from_lines(nil, 0, 1)
|
||||||
local lines = range:lines()
|
local lines = range:lines()
|
||||||
assert.are.same({ 'line one', 'and line two' }, lines)
|
assert.are.same({ 'line one', 'and line two' }, lines)
|
||||||
|
|
||||||
@@ -88,35 +88,35 @@ describe('Range', function()
|
|||||||
|
|
||||||
it('replace within line', function()
|
it('replace within line', function()
|
||||||
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
||||||
local range = Range.new(Pos.new(nil, 2, 5), Pos.new(nil, 2, 9), 'v')
|
local range = Range.new(Pos.new(nil, 1, 4), Pos.new(nil, 1, 8), 'v')
|
||||||
range:replace 'quack'
|
range:replace 'quack'
|
||||||
|
|
||||||
local text = Range.from_line(nil, 2):text()
|
local text = Range.from_line(nil, 1):text()
|
||||||
assert.are.same('the quack brown fox', text)
|
assert.are.same('the quack brown fox', text)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('delete within line', function()
|
it('delete within line', function()
|
||||||
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
||||||
local range = Range.new(Pos.new(nil, 2, 5), Pos.new(nil, 2, 10), 'v')
|
local range = Range.new(Pos.new(nil, 1, 4), Pos.new(nil, 1, 9), 'v')
|
||||||
range:replace ''
|
range:replace ''
|
||||||
|
|
||||||
local text = Range.from_line(nil, 2):text()
|
local text = Range.from_line(nil, 1):text()
|
||||||
assert.are.same('the brown fox', text)
|
assert.are.same('the brown fox', text)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
||||||
local range = Range.new(Pos.new(nil, 2, 5), Pos.new(nil, 2, 10), 'v')
|
local range = Range.new(Pos.new(nil, 1, 4), Pos.new(nil, 1, 9), 'v')
|
||||||
range:replace(nil)
|
range:replace(nil)
|
||||||
|
|
||||||
local text = Range.from_line(nil, 2):text()
|
local text = Range.from_line(nil, 1):text()
|
||||||
assert.are.same('the brown fox', text)
|
assert.are.same('the brown fox', text)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('replace across multiple lines: v', function()
|
it('replace across multiple lines: v', function()
|
||||||
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
||||||
local range = Range.new(Pos.new(nil, 2, 5), Pos.new(nil, 3, 5), 'v')
|
local range = Range.new(Pos.new(nil, 1, 4), Pos.new(nil, 2, 4), 'v')
|
||||||
range:replace 'plane flew'
|
range:replace 'plane flew'
|
||||||
|
|
||||||
local lines = Range.from_buf_text():lines()
|
local lines = Range.from_buf_text():lines()
|
||||||
@@ -130,7 +130,7 @@ describe('Range', function()
|
|||||||
|
|
||||||
it('replace a line', function()
|
it('replace a line', function()
|
||||||
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
||||||
local range = Range.from_line(nil, 2)
|
local range = Range.from_line(nil, 1)
|
||||||
range:replace 'the rabbit'
|
range:replace 'the rabbit'
|
||||||
|
|
||||||
local lines = Range.from_buf_text():lines()
|
local lines = Range.from_buf_text():lines()
|
||||||
@@ -145,7 +145,7 @@ describe('Range', function()
|
|||||||
|
|
||||||
it('replace multiple lines', function()
|
it('replace multiple lines', function()
|
||||||
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
||||||
local range = Range.from_lines(nil, 2, 3)
|
local range = Range.from_lines(nil, 1, 2)
|
||||||
range:replace 'the rabbit'
|
range:replace 'the rabbit'
|
||||||
|
|
||||||
local lines = Range.from_buf_text():lines()
|
local lines = Range.from_buf_text():lines()
|
||||||
@@ -159,7 +159,7 @@ describe('Range', function()
|
|||||||
|
|
||||||
it('delete single line', function()
|
it('delete single line', function()
|
||||||
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
||||||
local range = Range.from_line(nil, 2)
|
local range = Range.from_line(nil, 1)
|
||||||
range:replace(nil) -- delete lines
|
range:replace(nil) -- delete lines
|
||||||
|
|
||||||
local lines = Range.from_buf_text():lines()
|
local lines = Range.from_buf_text():lines()
|
||||||
@@ -173,7 +173,7 @@ describe('Range', function()
|
|||||||
|
|
||||||
it('delete multiple lines', function()
|
it('delete multiple lines', function()
|
||||||
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
|
||||||
local range = Range.from_lines(nil, 2, 3)
|
local range = Range.from_lines(nil, 1, 2)
|
||||||
range:replace(nil) -- delete lines
|
range:replace(nil) -- delete lines
|
||||||
|
|
||||||
local lines = Range.from_buf_text():lines()
|
local lines = Range.from_buf_text():lines()
|
||||||
@@ -187,179 +187,57 @@ describe('Range', function()
|
|||||||
it('text object: word', function()
|
it('text object: word', function()
|
||||||
withbuf({ 'the quick brown fox' }, function()
|
withbuf({ 'the quick brown fox' }, function()
|
||||||
vim.fn.setpos('.', { 0, 1, 5, 0 })
|
vim.fn.setpos('.', { 0, 1, 5, 0 })
|
||||||
assert.are.same('quick ', Range.from_motion('aw'):text())
|
assert.are.same('quick ', Range.from_text_object('aw'):text())
|
||||||
|
|
||||||
vim.fn.setpos('.', { 0, 1, 5, 0 })
|
vim.fn.setpos('.', { 0, 1, 5, 0 })
|
||||||
assert.are.same('quick', Range.from_motion('iw'):text())
|
assert.are.same('quick', Range.from_text_object('iw'):text())
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('text object: quote', function()
|
it('text object: quote', function()
|
||||||
withbuf({ [[the "quick" brown fox]] }, function()
|
withbuf({ [[the "quick" brown fox]] }, function()
|
||||||
vim.fn.setpos('.', { 0, 1, 5, 0 })
|
vim.fn.setpos('.', { 0, 1, 5, 0 })
|
||||||
assert.are.same('"quick"', Range.from_motion('a"'):text())
|
assert.are.same('"quick"', Range.from_text_object('a"'):text())
|
||||||
|
|
||||||
vim.fn.setpos('.', { 0, 1, 6, 0 })
|
vim.fn.setpos('.', { 0, 1, 6, 0 })
|
||||||
assert.are.same('quick', Range.from_motion('i"'):text())
|
assert.are.same('quick', Range.from_text_object('i"'):text())
|
||||||
end)
|
end)
|
||||||
|
|
||||||
withbuf({ [[the 'quick' brown fox]] }, function()
|
withbuf({ [[the 'quick' brown fox]] }, function()
|
||||||
vim.fn.setpos('.', { 0, 1, 5, 0 })
|
vim.fn.setpos('.', { 0, 1, 5, 0 })
|
||||||
assert.are.same("'quick'", Range.from_motion([[a']]):text())
|
assert.are.same("'quick'", Range.from_text_object([[a']]):text())
|
||||||
|
|
||||||
vim.fn.setpos('.', { 0, 1, 6, 0 })
|
vim.fn.setpos('.', { 0, 1, 6, 0 })
|
||||||
assert.are.same('quick', Range.from_motion([[i']]):text())
|
assert.are.same('quick', Range.from_text_object([[i']]):text())
|
||||||
end)
|
end)
|
||||||
|
|
||||||
withbuf({ [[the `quick` brown fox]] }, function()
|
withbuf({ [[the `quick` brown fox]] }, function()
|
||||||
vim.fn.setpos('.', { 0, 1, 5, 0 })
|
vim.fn.setpos('.', { 0, 1, 5, 0 })
|
||||||
assert.are.same('`quick`', Range.from_motion([[a`]]):text())
|
assert.are.same('`quick`', Range.from_text_object([[a`]]):text())
|
||||||
|
|
||||||
vim.fn.setpos('.', { 0, 1, 6, 0 })
|
vim.fn.setpos('.', { 0, 1, 6, 0 })
|
||||||
assert.are.same('quick', Range.from_motion([[i`]]):text())
|
assert.are.same('quick', Range.from_text_object([[i`]]):text())
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('text object: block', function()
|
it('text object: block', function()
|
||||||
withbuf({ 'this is a {', 'block', '} here' }, function()
|
withbuf({ 'this is a {', 'block', '} here' }, function()
|
||||||
vim.fn.setpos('.', { 0, 2, 1, 0 })
|
vim.fn.setpos('.', { 0, 2, 1, 0 })
|
||||||
assert.are.same('{\nblock\n}', Range.from_motion('a{'):text())
|
assert.are.same('{\nblock\n}', Range.from_text_object('a{'):text())
|
||||||
|
|
||||||
vim.fn.setpos('.', { 0, 2, 1, 0 })
|
vim.fn.setpos('.', { 0, 2, 1, 0 })
|
||||||
assert.are.same('block', Range.from_motion('i{'):text())
|
assert.are.same('block', Range.from_text_object('i{'):text())
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('text object: restores cursor position', function()
|
it('text object: restores cursor position', function()
|
||||||
withbuf({ 'this is a {block} here' }, function()
|
withbuf({ 'this is a {block} here' }, function()
|
||||||
vim.fn.setpos('.', { 0, 1, 13, 0 })
|
vim.fn.setpos('.', { 0, 1, 13, 0 })
|
||||||
assert.are.same('{block}', Range.from_motion('a{'):text())
|
assert.are.same('{block}', Range.from_text_object('a{'):text())
|
||||||
assert.are.same(vim.api.nvim_win_get_cursor(0), { 1, 12 })
|
assert.are.same(vim.api.nvim_win_get_cursor(0), { 1, 12 })
|
||||||
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 {',
|
||||||
@@ -380,18 +258,22 @@ describe('Range', function()
|
|||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('line', function()
|
it('line0', function()
|
||||||
withbuf({
|
withbuf({
|
||||||
'this is a {',
|
'this is a {',
|
||||||
'block',
|
'block',
|
||||||
'} here',
|
'} here',
|
||||||
}, function()
|
}, function()
|
||||||
local range = Range.new(Pos.new(0, 1, 6), Pos.new(0, 2, 5), 'v')
|
local range = Range.new(Pos.new(0, 0, 5), Pos.new(0, 1, 4), 'v')
|
||||||
local lfirst = assert(range:line(1), 'lfirst null')
|
local lfirst = range:line0(0)
|
||||||
assert.are.same('is a {', lfirst:text())
|
assert.are.same(5, lfirst.idx0.start)
|
||||||
assert.are.same(Pos.new(0, 1, 6), lfirst.start)
|
assert.are.same(10, lfirst.idx0.stop)
|
||||||
assert.are.same(Pos.new(0, 1, 11), lfirst.stop)
|
assert.are.same(0, lfirst.lnum)
|
||||||
assert.are.same('block', range:line(2):text())
|
assert.are.same('is a {', lfirst.text())
|
||||||
|
assert.are.same('is a {', lfirst.range():text())
|
||||||
|
assert.are.same(Pos.new(0, 0, 5), lfirst.range().start)
|
||||||
|
assert.are.same(Pos.new(0, 0, 10), lfirst.range().stop)
|
||||||
|
assert.are.same('block', range:line0(1).text())
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -415,16 +297,16 @@ describe('Range', function()
|
|||||||
vim.cmd.normal 'v' -- enter visual mode
|
vim.cmd.normal 'v' -- enter visual mode
|
||||||
vim.cmd.normal 'l' -- select one character to the right
|
vim.cmd.normal 'l' -- select one character to the right
|
||||||
local range = Range.from_vtext()
|
local range = Range.from_vtext()
|
||||||
assert.are.same(range.start, Pos.new(nil, 1, 3))
|
assert.are.same(range.start, Pos.new(nil, 0, 2))
|
||||||
assert.are.same(range.stop, Pos.new(nil, 1, 4))
|
assert.are.same(range.stop, Pos.new(nil, 0, 3))
|
||||||
assert.are.same(range.mode, 'v')
|
assert.are.same(range.mode, 'v')
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('from_op_func', function()
|
it('from_op_func', function()
|
||||||
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, 0, 0)
|
||||||
local b = Pos.new(nil, 2, 2)
|
local b = Pos.new(nil, 1, 1)
|
||||||
a:save_to_pos "'["
|
a:save_to_pos "'["
|
||||||
b:save_to_pos "']"
|
b:save_to_pos "']"
|
||||||
|
|
||||||
@@ -435,94 +317,23 @@ describe('Range', function()
|
|||||||
|
|
||||||
range = Range.from_op_func 'line'
|
range = Range.from_op_func 'line'
|
||||||
assert.are.same(range.start, a)
|
assert.are.same(range.start, a)
|
||||||
assert.are.same(range.stop, Pos.new(nil, 2, Pos.MAX_COL))
|
assert.are.same(range.stop, Pos.new(nil, 1, Pos.MAX_COL))
|
||||||
assert.are.same(range.mode, 'V')
|
assert.are.same(range.mode, 'V')
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('from_cmd_args: range=0', function()
|
it('from_cmd_args', function()
|
||||||
local args = { range = 0 }
|
local args = { range = 1 }
|
||||||
withbuf(
|
|
||||||
{ 'line one', 'and line two' },
|
|
||||||
function() assert.are.same(Range.from_cmd_args(args), nil) end
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('from_cmd_args: range=1', function()
|
|
||||||
local args = { range = 1, line1 = 1 }
|
|
||||||
withbuf({ 'line one', 'and line two' }, function()
|
withbuf({ 'line one', 'and line two' }, function()
|
||||||
local a = Pos.new(nil, 1, 1)
|
local a = Pos.new(nil, 0, 0)
|
||||||
local b = Pos.new(nil, 1, Pos.MAX_COL)
|
local b = Pos.new(nil, 1, 1)
|
||||||
|
|
||||||
local range = Range.from_cmd_args(args) --[[@as u.Range]]
|
|
||||||
assert.are.same(range.start, a)
|
|
||||||
assert.are.same(range.stop, b)
|
|
||||||
assert.are.same(range.mode, 'V')
|
|
||||||
assert.are.same(range:text(), 'line one')
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('from_cmd_args: range=2: no-visual', function()
|
|
||||||
local args = { range = 2, line1 = 1, line2 = 2 }
|
|
||||||
withbuf({ 'line one', 'and line two' }, function()
|
|
||||||
local range = Range.from_cmd_args(args) --[[@as u.Range]]
|
|
||||||
assert.are.same(range.start, Pos.new(nil, 1, 1))
|
|
||||||
assert.are.same(range.stop, Pos.new(nil, 2, Pos.MAX_COL))
|
|
||||||
assert.are.same(range.mode, 'V')
|
|
||||||
assert.are.same(range:text(), 'line one\nand line two')
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('from_cmd_args: range=2: visual: linewise', function()
|
|
||||||
local args = { range = 2, line1 = 1, line2 = 2 }
|
|
||||||
withbuf({ 'line one', 'and line two' }, function()
|
|
||||||
local a = Pos.new(nil, 1, 1)
|
|
||||||
local b = Pos.new(nil, 2, Pos.MAX_COL)
|
|
||||||
a:save_to_pos "'<"
|
a:save_to_pos "'<"
|
||||||
b:save_to_pos "'>"
|
b:save_to_pos "'>"
|
||||||
local range = Range.from_cmd_args(args) --[[@as u.Range]]
|
|
||||||
assert.are.same(range.start, a)
|
|
||||||
assert.are.same(range.stop, b)
|
|
||||||
assert.are.same(range.mode, 'V')
|
|
||||||
assert.are.same(range:text(), 'line one\nand line two')
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('from_cmd_args: range=2: visual: charwise', function()
|
local range = Range.from_cmd_args(args)
|
||||||
local args = { range = 2, line1 = 1, line2 = 1 }
|
|
||||||
withbuf({ 'line one', 'and line two' }, function()
|
|
||||||
-- Simulate a visual selection:
|
|
||||||
local a = Pos.new(nil, 1, 1)
|
|
||||||
local b = Pos.new(nil, 1, 4)
|
|
||||||
a:save_to_pos "'<"
|
|
||||||
b:save_to_pos "'>"
|
|
||||||
Range.new(a, b, 'v'):set_visual_selection()
|
|
||||||
assert.are.same(vim.fn.mode(), 'v')
|
|
||||||
|
|
||||||
-- In this simulated setup, we need to force visualmode() to return
|
|
||||||
-- 'v' and histget() to return [['<,'>]]:
|
|
||||||
|
|
||||||
-- visualmode()
|
|
||||||
local orig_visualmode = vim.fn.visualmode
|
|
||||||
--- @diagnostic disable-next-line: duplicate-set-field
|
|
||||||
function vim.fn.visualmode() return 'v' end
|
|
||||||
assert.are.same(vim.fn.visualmode(), 'v')
|
|
||||||
|
|
||||||
-- histget()
|
|
||||||
local orig_histget = vim.fn.histget
|
|
||||||
--- @diagnostic disable-next-line: duplicate-set-field
|
|
||||||
function vim.fn.histget(x, y) return [['<,'>]] end
|
|
||||||
|
|
||||||
-- Now run the test:
|
|
||||||
local range = Range.from_cmd_args(args) --[[@as u.Range]]
|
|
||||||
assert.are.same(range.start, a)
|
assert.are.same(range.start, a)
|
||||||
assert.are.same(range.stop, b)
|
assert.are.same(range.stop, b)
|
||||||
assert.are.same(range.mode, 'v')
|
assert.are.same(range.mode, 'v')
|
||||||
assert.are.same(range:text(), 'line')
|
|
||||||
|
|
||||||
-- Reset visualmode() and histget():
|
|
||||||
vim.fn.visualmode = orig_visualmode
|
|
||||||
vim.fn.histget = orig_histget
|
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -530,25 +341,25 @@ describe('Range', function()
|
|||||||
withbuf({ [[the "quick" brown fox]] }, function()
|
withbuf({ [[the "quick" brown fox]] }, function()
|
||||||
vim.fn.setpos('.', { 0, 1, 5, 0 })
|
vim.fn.setpos('.', { 0, 1, 5, 0 })
|
||||||
local range = Range.find_nearest_quotes()
|
local range = Range.find_nearest_quotes()
|
||||||
assert.are.same(range.start, Pos.new(nil, 1, 5))
|
assert.are.same(range.start, Pos.new(nil, 0, 4))
|
||||||
assert.are.same(range.stop, Pos.new(nil, 1, 11))
|
assert.are.same(range.stop, Pos.new(nil, 0, 10))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
withbuf({ [[the 'quick' brown fox]] }, function()
|
withbuf({ [[the 'quick' brown fox]] }, function()
|
||||||
vim.fn.setpos('.', { 0, 1, 5, 0 })
|
vim.fn.setpos('.', { 0, 1, 5, 0 })
|
||||||
local range = Range.find_nearest_quotes()
|
local range = Range.find_nearest_quotes()
|
||||||
assert.are.same(range.start, Pos.new(nil, 1, 5))
|
assert.are.same(range.start, Pos.new(nil, 0, 4))
|
||||||
assert.are.same(range.stop, Pos.new(nil, 1, 11))
|
assert.are.same(range.stop, Pos.new(nil, 0, 10))
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('smallest', function()
|
it('smallest', function()
|
||||||
local r1 = Range.new(Pos.new(nil, 1, 2), Pos.new(nil, 1, 4), 'v')
|
local r1 = Range.new(Pos.new(nil, 0, 1), Pos.new(nil, 0, 3), 'v')
|
||||||
local r2 = Range.new(Pos.new(nil, 1, 3), Pos.new(nil, 1, 5), 'v')
|
local r2 = Range.new(Pos.new(nil, 0, 2), Pos.new(nil, 0, 4), 'v')
|
||||||
local r3 = Range.new(Pos.new(nil, 1, 1), Pos.new(nil, 1, 6), 'v')
|
local r3 = Range.new(Pos.new(nil, 0, 0), Pos.new(nil, 0, 5), 'v')
|
||||||
local smallest = Range.smallest { r1, r2, r3 }
|
local smallest = Range.smallest { r1, r2, r3 }
|
||||||
assert.are.same(smallest.start, Pos.new(nil, 1, 2))
|
assert.are.same(smallest.start, Pos.new(nil, 0, 1))
|
||||||
assert.are.same(smallest.stop, Pos.new(nil, 1, 4))
|
assert.are.same(smallest.stop, Pos.new(nil, 0, 3))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('clone', function()
|
it('clone', function()
|
||||||
@@ -570,9 +381,9 @@ describe('Range', function()
|
|||||||
|
|
||||||
it('to_linewise()', function()
|
it('to_linewise()', function()
|
||||||
withbuf({ 'line one', 'and line two' }, function()
|
withbuf({ 'line one', 'and line two' }, function()
|
||||||
local range = Range.new(Pos.new(nil, 1, 2), Pos.new(nil, 2, 4), 'v')
|
local range = Range.new(Pos.new(nil, 0, 1), Pos.new(nil, 1, 3), 'v')
|
||||||
local linewise_range = range:to_linewise()
|
local linewise_range = range:to_linewise()
|
||||||
assert.are.same(linewise_range.start.col, 1)
|
assert.are.same(linewise_range.start.col, 0)
|
||||||
assert.are.same(linewise_range.stop.col, Pos.MAX_COL)
|
assert.are.same(linewise_range.stop.col, Pos.MAX_COL)
|
||||||
assert.are.same(linewise_range.mode, 'V')
|
assert.are.same(linewise_range.mode, 'V')
|
||||||
end)
|
end)
|
||||||
@@ -580,133 +391,82 @@ describe('Range', function()
|
|||||||
|
|
||||||
it('is_empty', function()
|
it('is_empty', function()
|
||||||
withbuf({ 'line one', 'and line two' }, function()
|
withbuf({ 'line one', 'and line two' }, function()
|
||||||
local range = Range.new(Pos.new(nil, 1, 1), nil, 'v')
|
local range = Range.new(Pos.new(nil, 0, 0), nil, 'v')
|
||||||
assert.is_true(range:is_empty())
|
assert.is_true(range:is_empty())
|
||||||
|
|
||||||
local range2 = Range.new(Pos.new(nil, 1, 1), Pos.new(nil, 1, 2), 'v')
|
local range2 = Range.new(Pos.new(nil, 0, 0), Pos.new(nil, 0, 1), 'v')
|
||||||
assert.is_false(range2:is_empty())
|
assert.is_false(range2:is_empty())
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('trim_start', function()
|
it('trim_start', function()
|
||||||
withbuf({ ' line one', 'line two' }, function()
|
withbuf({ ' line one', 'line two' }, function()
|
||||||
local range = Range.new(Pos.new(nil, 1, 1), Pos.new(nil, 1, 10), 'v')
|
local range = Range.new(Pos.new(nil, 0, 0), Pos.new(nil, 0, 9), 'v')
|
||||||
local trimmed = range:trim_start()
|
local trimmed = range:trim_start()
|
||||||
assert.are.same(trimmed.start, Pos.new(nil, 1, 4)) -- should be after the spaces
|
assert.are.same(trimmed.start, Pos.new(nil, 0, 3)) -- should be after the spaces
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('trim_stop', function()
|
it('trim_stop', function()
|
||||||
withbuf({ 'line one ', 'line two' }, function()
|
withbuf({ 'line one ', 'line two' }, function()
|
||||||
local range = Range.new(Pos.new(nil, 1, 1), Pos.new(nil, 1, 10), 'v')
|
local range = Range.new(Pos.new(nil, 0, 0), Pos.new(nil, 0, 9), 'v')
|
||||||
local trimmed = range:trim_stop()
|
local trimmed = range:trim_stop()
|
||||||
assert.are.same(trimmed.stop, Pos.new(nil, 1, 8)) -- should be before the spaces
|
assert.are.same(trimmed.stop, Pos.new(nil, 0, 7)) -- should be before the spaces
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('contains', function()
|
it('contains', function()
|
||||||
withbuf({ 'line one', 'and line two' }, function()
|
withbuf({ 'line one', 'and line two' }, function()
|
||||||
local range = Range.new(Pos.new(nil, 1, 2), Pos.new(nil, 1, 4), 'v')
|
local range = Range.new(Pos.new(nil, 0, 1), Pos.new(nil, 0, 3), 'v')
|
||||||
local pos = Pos.new(nil, 1, 3)
|
local pos = Pos.new(nil, 0, 2)
|
||||||
assert.is_true(range:contains(pos))
|
assert.is_true(range:contains(pos))
|
||||||
|
|
||||||
pos = Pos.new(nil, 1, 5) -- outside of range
|
pos = Pos.new(nil, 0, 4) -- outside of range
|
||||||
assert.is_false(range:contains(pos))
|
assert.is_false(range:contains(pos))
|
||||||
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, 0, 1), Pos.new(nil, 1, 3), 'v')
|
||||||
local shrunk = range:shrink(1)
|
local shrunk = range:shrink(1)
|
||||||
assert.are.same(shrunk.start, Pos.new(nil, 2, 4))
|
assert.are.same(shrunk.start, Pos.new(nil, 0, 2))
|
||||||
assert.are.same(shrunk.stop, Pos.new(nil, 3, 4))
|
assert.are.same(shrunk.stop, Pos.new(nil, 1, 2))
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('must_shrink', function()
|
it('must_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, 0, 1), Pos.new(nil, 1, 3), 'v')
|
||||||
local shrunk = range:must_shrink(1)
|
local shrunk = range:must_shrink(1)
|
||||||
assert.are.same(shrunk.start, Pos.new(nil, 2, 4))
|
assert.are.same(shrunk.start, Pos.new(nil, 0, 2))
|
||||||
assert.are.same(shrunk.stop, Pos.new(nil, 3, 4))
|
assert.are.same(shrunk.stop, Pos.new(nil, 1, 2))
|
||||||
|
|
||||||
assert.has.error(
|
assert.has.error(function() range:must_shrink(100) end, 'error in Range:must_shrink: Range:shrink() returned nil')
|
||||||
function() range:must_shrink(100) end,
|
|
||||||
'error in Range:must_shrink: Range:shrink() returned nil'
|
|
||||||
)
|
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('set_visual_selection', function()
|
it('set_visual_selection', function()
|
||||||
withbuf({ 'line one', 'and line two' }, function()
|
withbuf({ 'line one', 'and line two' }, function()
|
||||||
local range = Range.from_lines(nil, 1, 2)
|
local range = Range.from_lines(nil, 0, 1)
|
||||||
range:set_visual_selection()
|
range:set_visual_selection()
|
||||||
|
|
||||||
assert.are.same(Pos.from_pos 'v', Pos.new(nil, 1, 1))
|
assert.are.same(Pos.from_pos 'v', Pos.new(nil, 0, 0))
|
||||||
-- Since the selection is 'V' (instead of 'v'), the end
|
assert.are.same(Pos.from_pos '.', Pos.new(nil, 1, 11))
|
||||||
-- selects one character past the end:
|
|
||||||
assert.are.same(Pos.from_pos '.', Pos.new(nil, 2, 13))
|
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('selections set to past the EOL should not error', function()
|
it('selections set to past the EOL should not error', function()
|
||||||
withbuf({ 'Rg SET NAMES' }, function()
|
withbuf({ 'Rg SET NAMES' }, function()
|
||||||
local b = vim.api.nvim_get_current_buf()
|
local b = vim.api.nvim_get_current_buf()
|
||||||
local r = Range.new(Pos.new(b, 1, 4), Pos.new(b, 1, 13), 'v')
|
local r = Range.new(Pos.new(b, 0, 3), Pos.new(b, 0, 12), 'v')
|
||||||
r:replace 'bleh'
|
r:replace 'bleh'
|
||||||
assert.are.same({ 'Rg bleh' }, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
assert.are.same({ 'Rg bleh' }, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
withbuf({ 'Rg SET NAMES' }, function()
|
withbuf({ 'Rg SET NAMES' }, function()
|
||||||
local b = vim.api.nvim_get_current_buf()
|
local b = vim.api.nvim_get_current_buf()
|
||||||
local r = Range.new(Pos.new(b, 1, 4), Pos.new(b, 1, 12), 'v')
|
local r = Range.new(Pos.new(b, 0, 3), Pos.new(b, 0, 11), 'v')
|
||||||
r:replace 'bleh'
|
r:replace 'bleh'
|
||||||
assert.are.same({ 'Rg bleh' }, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
assert.are.same({ 'Rg bleh' }, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
||||||
end)
|
end)
|
||||||
@@ -715,19 +475,13 @@ describe('Range', function()
|
|||||||
it('replace updates Range.stop: same line', function()
|
it('replace updates Range.stop: same line', function()
|
||||||
withbuf({ 'The quick brown fox jumps over the lazy dog' }, function()
|
withbuf({ 'The quick brown fox jumps over the lazy dog' }, function()
|
||||||
local b = vim.api.nvim_get_current_buf()
|
local b = vim.api.nvim_get_current_buf()
|
||||||
local r = Range.new(Pos.new(b, 1, 5), Pos.new(b, 1, 9), 'v')
|
local r = Range.new(Pos.new(b, 0, 4), Pos.new(b, 0, 8), 'v')
|
||||||
|
|
||||||
r:replace 'bleh1'
|
r:replace 'bleh1'
|
||||||
assert.are.same(
|
assert.are.same({ 'The bleh1 brown fox jumps over the lazy dog' }, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
||||||
{ '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(
|
assert.are.same({ 'The bleh2 brown fox jumps over the lazy dog' }, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
||||||
{ 'The bleh2 brown fox jumps over the lazy dog' },
|
|
||||||
vim.api.nvim_buf_get_lines(b, 0, -1, false)
|
|
||||||
)
|
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -737,21 +491,14 @@ describe('Range', function()
|
|||||||
'over the lazy dog',
|
'over the lazy dog',
|
||||||
}, function()
|
}, function()
|
||||||
local b = vim.api.nvim_get_current_buf()
|
local b = vim.api.nvim_get_current_buf()
|
||||||
local r = Range.new(Pos.new(b, 1, 21), Pos.new(b, 2, 4), 'v')
|
local r = Range.new(Pos.new(b, 0, 20), Pos.new(b, 1, 3), 'v')
|
||||||
assert.are.same({ 'jumps', 'over' }, r:lines())
|
assert.are.same({ 'jumps', 'over' }, r:lines())
|
||||||
|
|
||||||
r:replace 'bleh1'
|
r:replace 'bleh1'
|
||||||
assert.are.same(
|
assert.are.same({ 'The quick brown fox bleh1 the lazy dog' }, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
||||||
{ 'The quick brown fox bleh1 the lazy dog' },
|
|
||||||
vim.api.nvim_buf_get_lines(b, 0, -1, false)
|
|
||||||
)
|
|
||||||
assert.are.same({ 'bleh1' }, r:lines())
|
|
||||||
|
|
||||||
r:replace 'blehGoo2'
|
r:replace 'blehGoo2'
|
||||||
assert.are.same(
|
assert.are.same({ 'The quick brown fox blehGoo2 the lazy dog' }, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
||||||
{ 'The quick brown fox blehGoo2 the lazy dog' },
|
|
||||||
vim.api.nvim_buf_get_lines(b, 0, -1, false)
|
|
||||||
)
|
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -764,7 +511,7 @@ describe('Range', function()
|
|||||||
'the lazy dog',
|
'the lazy dog',
|
||||||
}, function()
|
}, function()
|
||||||
local b = vim.api.nvim_get_current_buf()
|
local b = vim.api.nvim_get_current_buf()
|
||||||
local r = Range.new(Pos.new(b, 2, 1), Pos.new(b, 4, Pos.MAX_COL), 'V')
|
local r = Range.new(Pos.new(b, 1, 0), Pos.new(b, 3, Pos.MAX_COL), 'V')
|
||||||
assert.are.same({ 'fox', 'jumps', 'over' }, r:lines())
|
assert.are.same({ 'fox', 'jumps', 'over' }, r:lines())
|
||||||
|
|
||||||
r:replace { 'bleh1', 'bleh2' }
|
r:replace { 'bleh1', 'bleh2' }
|
||||||
@@ -793,7 +540,7 @@ describe('Range', function()
|
|||||||
'the lazy dog',
|
'the lazy dog',
|
||||||
}, function()
|
}, function()
|
||||||
local b = vim.api.nvim_get_current_buf()
|
local b = vim.api.nvim_get_current_buf()
|
||||||
local r = Range.new(Pos.new(b, 2, 1), Pos.new(b, 4, Pos.MAX_COL), 'V')
|
local r = Range.new(Pos.new(b, 1, 0), Pos.new(b, 3, Pos.MAX_COL), 'V')
|
||||||
assert.are.same({ 'fox', 'jumps', 'over' }, r:lines())
|
assert.are.same({ 'fox', 'jumps', 'over' }, r:lines())
|
||||||
|
|
||||||
r:replace(nil)
|
r:replace(nil)
|
||||||
@@ -810,86 +557,4 @@ 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)
|
||||||
|
|||||||
@@ -1,297 +0,0 @@
|
|||||||
local R = require 'u.renderer'
|
|
||||||
local withbuf = loadfile './spec/withbuf.lua'()
|
|
||||||
|
|
||||||
local function getlines() return vim.api.nvim_buf_get_lines(0, 0, -1, true) end
|
|
||||||
|
|
||||||
describe('Renderer', function()
|
|
||||||
it('should render text in an empty buffer', function()
|
|
||||||
withbuf({}, function()
|
|
||||||
local r = R.Renderer.new(0)
|
|
||||||
r:render { 'hello', ' ', 'world' }
|
|
||||||
assert.are.same(getlines(), { 'hello world' })
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('should result in the correct text after repeated renders', function()
|
|
||||||
withbuf({}, function()
|
|
||||||
local r = R.Renderer.new(0)
|
|
||||||
r:render { 'hello', ' ', 'world' }
|
|
||||||
assert.are.same(getlines(), { 'hello world' })
|
|
||||||
|
|
||||||
r:render { 'goodbye', ' ', 'world' }
|
|
||||||
assert.are.same(getlines(), { 'goodbye world' })
|
|
||||||
|
|
||||||
r:render { 'hello', ' ', 'universe' }
|
|
||||||
assert.are.same(getlines(), { 'hello universe' })
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('should handle tags correctly', function()
|
|
||||||
withbuf({}, function()
|
|
||||||
local r = R.Renderer.new(0)
|
|
||||||
r:render {
|
|
||||||
R.h('text', { hl = 'HighlightGroup' }, 'hello '),
|
|
||||||
R.h('text', { hl = 'HighlightGroup' }, 'world'),
|
|
||||||
}
|
|
||||||
assert.are.same(getlines(), { 'hello world' })
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('should reconcile added lines', function()
|
|
||||||
withbuf({}, function()
|
|
||||||
local r = R.Renderer.new(0)
|
|
||||||
r:render { 'line 1', '\n', 'line 2' }
|
|
||||||
assert.are.same(getlines(), { 'line 1', 'line 2' })
|
|
||||||
|
|
||||||
-- Add a new line:
|
|
||||||
r:render { 'line 1', '\n', 'line 2\n', 'line 3' }
|
|
||||||
assert.are.same(getlines(), { 'line 1', 'line 2', 'line 3' })
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('should reconcile deleted lines', function()
|
|
||||||
withbuf({}, function()
|
|
||||||
local r = R.Renderer.new(0)
|
|
||||||
r:render { 'line 1', '\nline 2', '\nline 3' }
|
|
||||||
assert.are.same(getlines(), { 'line 1', 'line 2', 'line 3' })
|
|
||||||
|
|
||||||
-- Remove a line:
|
|
||||||
r:render { 'line 1', '\nline 3' }
|
|
||||||
assert.are.same(getlines(), { 'line 1', 'line 3' })
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('should handle multiple nested elements', function()
|
|
||||||
withbuf({}, function()
|
|
||||||
local r = R.Renderer.new(0)
|
|
||||||
r:render {
|
|
||||||
R.h('text', {}, {
|
|
||||||
'first line',
|
|
||||||
}),
|
|
||||||
'\n',
|
|
||||||
R.h('text', {}, 'second line'),
|
|
||||||
}
|
|
||||||
assert.are.same(getlines(), { 'first line', 'second line' })
|
|
||||||
|
|
||||||
r:render {
|
|
||||||
R.h('text', {}, 'updated first line'),
|
|
||||||
'\n',
|
|
||||||
R.h('text', {}, 'third line'),
|
|
||||||
}
|
|
||||||
assert.are.same(getlines(), { 'updated first line', 'third line' })
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
--
|
|
||||||
-- get_tags_at
|
|
||||||
--
|
|
||||||
|
|
||||||
it('should return no extmarks for an empty buffer', function()
|
|
||||||
withbuf({}, function()
|
|
||||||
local r = R.Renderer.new(0)
|
|
||||||
local pos_infos = r:get_tags_at { 0, 0 }
|
|
||||||
assert.are.same(pos_infos, {})
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('should return correct extmark for a given position', function()
|
|
||||||
withbuf({}, function()
|
|
||||||
local r = R.Renderer.new(0)
|
|
||||||
r:render {
|
|
||||||
R.h('text', { hl = 'HighlightGroup1' }, 'Hello'),
|
|
||||||
R.h('text', { hl = 'HighlightGroup2' }, ' World'),
|
|
||||||
}
|
|
||||||
|
|
||||||
local pos_infos = r:get_tags_at { 0, 2 }
|
|
||||||
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, 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)
|
|
||||||
|
|
||||||
it('should return multiple extmarks for overlapping text', function()
|
|
||||||
withbuf({}, function()
|
|
||||||
local r = R.Renderer.new(0)
|
|
||||||
r:render {
|
|
||||||
R.h('text', { hl = 'HighlightGroup1' }, {
|
|
||||||
'Hello',
|
|
||||||
R.h(
|
|
||||||
'text',
|
|
||||||
{ hl = 'HighlightGroup2', extmark = { hl_group = 'HighlightGroup2' } },
|
|
||||||
' World'
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
local pos_infos = r:get_tags_at { 0, 5 }
|
|
||||||
|
|
||||||
assert.are.same(#pos_infos, 2)
|
|
||||||
assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup2')
|
|
||||||
assert.are.same(pos_infos[2].tag.attributes.hl, 'HighlightGroup1')
|
|
||||||
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)
|
|
||||||
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 test_signal = Signal:new(5)
|
local signal = Signal:new(5)
|
||||||
local mapped_signal = test_signal:map(function(value) return value * 2 end)
|
local mapped_signal = 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
|
||||||
test_signal:set(10)
|
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 test_signal = Signal:new(nil)
|
local signal = Signal:new(nil)
|
||||||
local mapped_signal = test_signal:map(function(value) return value or 'default' end)
|
local mapped_signal = 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
|
||||||
test_signal:set 'new value'
|
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 test_signal = Signal:new(5)
|
local signal = Signal:new(5)
|
||||||
local filtered_signal = test_signal:filter(function(value) return value > 10 end)
|
local filtered_signal = 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
|
||||||
test_signal:set(15)
|
signal:set(15)
|
||||||
assert.is.equal(filtered_signal:get(), 15) -- Now filtered
|
assert.is.equal(filtered_signal:get(), 15) -- Now filtered
|
||||||
test_signal:set(8)
|
signal:set(8)
|
||||||
assert.is.equal(filtered_signal:get(), 15) -- Does not pass the filter
|
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 test_signal = Signal:new(nil)
|
local signal = Signal:new(nil)
|
||||||
local filtered_signal = test_signal:filter(function(value) return value ~= nil end)
|
local filtered_signal = 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
|
||||||
test_signal:set(10)
|
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 test_signal = Signal:new(2)
|
local signal = Signal:new(2)
|
||||||
local memoized_signal = tracker.create_memo(function() return test_signal:get() * 2 end)
|
local memoized_signal = tracker.create_memo(function() return 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
|
||||||
|
|
||||||
test_signal:set(3)
|
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
|
||||||
|
|
||||||
test_signal:set(5)
|
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 test_signal = Signal:new(10)
|
local 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 test_signal:get() + 1
|
return 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
|
||||||
|
|
||||||
test_signal:set(10) -- Set the same value
|
signal:set(10) -- Set the same value
|
||||||
assert.is.equal(memoized_signal:get(), 11)
|
assert.is.equal(memoized_signal:get(), 11)
|
||||||
assert.is.equal(call_count, 2)
|
assert.is.equal(call_count, 2)
|
||||||
|
|
||||||
test_signal:set(20)
|
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 test_signal = Signal:new(5)
|
local signal = Signal:new(5)
|
||||||
local call_count = 0
|
local call_count = 0
|
||||||
|
|
||||||
tracker.create_effect(function()
|
tracker.create_effect(function()
|
||||||
test_signal:get() -- track as a dependency
|
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)
|
||||||
test_signal:set(10)
|
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 test_signal = Signal:new(5)
|
local 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 test_signal:get() * 2
|
return 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
|
||||||
test_signal:set(10) -- Update signal value
|
signal:set(10) -- Update signal value
|
||||||
assert.is.equal(call_count, 1) -- Callback should not be called again
|
assert.is.equal(call_count, 1) -- Callback should not be called again
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|||||||
70
spec/utils_spec.lua
Normal file
70
spec/utils_spec.lua
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
local utils = require 'u.utils'
|
||||||
|
|
||||||
|
--- @param s string
|
||||||
|
local function split(s) return vim.split(s, '') end
|
||||||
|
|
||||||
|
--- @param original string
|
||||||
|
--- @param changes LevenshteinChange[]
|
||||||
|
local function morph(original, changes)
|
||||||
|
local t = split(original)
|
||||||
|
for _, change in ipairs(changes) do
|
||||||
|
if change.kind == 'add' then
|
||||||
|
table.insert(t, change.index, change.item)
|
||||||
|
elseif change.kind == 'delete' then
|
||||||
|
table.remove(t, change.index)
|
||||||
|
elseif change.kind == 'change' then
|
||||||
|
t[change.index] = change.to
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return vim.iter(t):join ''
|
||||||
|
end
|
||||||
|
|
||||||
|
describe('utils', function()
|
||||||
|
it('levenshtein', function()
|
||||||
|
local original = 'abc'
|
||||||
|
local result = 'absece'
|
||||||
|
local changes = utils.levenshtein(split(original), split(result))
|
||||||
|
assert.are.same(changes, {
|
||||||
|
{
|
||||||
|
item = 'e',
|
||||||
|
kind = 'add',
|
||||||
|
index = 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
item = 'e',
|
||||||
|
kind = 'add',
|
||||||
|
index = 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
item = 's',
|
||||||
|
kind = 'add',
|
||||||
|
index = 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.are.same(morph(original, changes), result)
|
||||||
|
|
||||||
|
original = 'jonathan'
|
||||||
|
result = 'ajoanthan'
|
||||||
|
changes = utils.levenshtein(split(original), split(result))
|
||||||
|
assert.are.same(changes, {
|
||||||
|
{
|
||||||
|
from = 'a',
|
||||||
|
index = 4,
|
||||||
|
kind = 'change',
|
||||||
|
to = 'n',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from = 'n',
|
||||||
|
index = 3,
|
||||||
|
kind = 'change',
|
||||||
|
to = 'a',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index = 1,
|
||||||
|
item = 'a',
|
||||||
|
kind = 'add',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.are.same(morph(original, changes), result)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
require 'luacov'
|
|
||||||
local function withbuf(lines, f)
|
local function withbuf(lines, f)
|
||||||
vim.go.swapfile = false
|
vim.go.swapfile = false
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
syntax = "LuaJIT"
|
|
||||||
call_parentheses = "None"
|
call_parentheses = "None"
|
||||||
collapse_simple_statement = "Always"
|
collapse_simple_statement = "Always"
|
||||||
column_width = 100
|
column_width = 120
|
||||||
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
Normal file
36
vim.yml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
base: lua51
|
||||||
|
globals:
|
||||||
|
vim:
|
||||||
|
any: true
|
||||||
|
assert.are.same:
|
||||||
|
args:
|
||||||
|
- type: any
|
||||||
|
- type: any
|
||||||
|
assert.are_not.same:
|
||||||
|
args:
|
||||||
|
- type: any
|
||||||
|
- type: any
|
||||||
|
assert.has.error:
|
||||||
|
args:
|
||||||
|
- type: any
|
||||||
|
- type: any
|
||||||
|
assert.is_true:
|
||||||
|
args:
|
||||||
|
- type: any
|
||||||
|
- type: any
|
||||||
|
assert.is_false:
|
||||||
|
args:
|
||||||
|
- type: any
|
||||||
|
- type: any
|
||||||
|
describe:
|
||||||
|
args:
|
||||||
|
- type: string
|
||||||
|
- type: function
|
||||||
|
it:
|
||||||
|
args:
|
||||||
|
- type: string
|
||||||
|
- type: function
|
||||||
|
before_each:
|
||||||
|
args:
|
||||||
|
- type: function
|
||||||
Reference in New Issue
Block a user