mirror of
https://github.com/nvim-neotest/nvim-nio.git
synced 2024-09-16 14:24:04 +02:00
feat: initial plugin
This commit is contained in:
parent
b6448cb770
commit
d6f92542f1
34 changed files with 7245 additions and 0 deletions
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: rcarriga
|
103
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
103
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,103 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG]"
|
||||
labels: ''
|
||||
assignees: rcarriga
|
||||
|
||||
---
|
||||
|
||||
**NeoVim Version**
|
||||
Output of `nvim --version`
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Please provide a minimal `init.lua` to reproduce which can be run as the following:
|
||||
```sh
|
||||
nvim --clean -u minimal.lua
|
||||
```
|
||||
|
||||
You can edit the following example file to include your adapters and other required setup.
|
||||
```lua
|
||||
-- ignore default config and plugins
|
||||
vim.opt.runtimepath:remove(vim.fn.expand("~/.config/nvim"))
|
||||
vim.opt.packpath:remove(vim.fn.expand("~/.local/share/nvim/site"))
|
||||
vim.opt.termguicolors = true
|
||||
|
||||
-- append test directory
|
||||
local test_dir = "/tmp/nvim-config"
|
||||
vim.opt.runtimepath:append(vim.fn.expand(test_dir))
|
||||
vim.opt.packpath:append(vim.fn.expand(test_dir))
|
||||
|
||||
-- install packer
|
||||
local install_path = test_dir .. "/pack/packer/start/packer.nvim"
|
||||
local install_plugins = false
|
||||
|
||||
if vim.fn.empty(vim.fn.glob(install_path)) > 0 then
|
||||
vim.cmd("!git clone https://github.com/wbthomason/packer.nvim " .. install_path)
|
||||
vim.cmd("packadd packer.nvim")
|
||||
install_plugins = true
|
||||
end
|
||||
|
||||
local packer = require("packer")
|
||||
|
||||
packer.init({
|
||||
package_root = test_dir .. "/pack",
|
||||
compile_path = test_dir .. "/plugin/packer_compiled.lua",
|
||||
})
|
||||
|
||||
packer.startup(function(use)
|
||||
use("wbthomason/packer.nvim")
|
||||
|
||||
use({
|
||||
"nvim-neotest/neotest",
|
||||
requires = {
|
||||
"vim-test/vim-test",
|
||||
"nvim-lua/plenary.nvim",
|
||||
"nvim-treesitter/nvim-treesitter",
|
||||
"antoinemadec/FixCursorHold.nvim",
|
||||
},
|
||||
config = function()
|
||||
require("neotest").setup({
|
||||
adapters = {},
|
||||
})
|
||||
end,
|
||||
})
|
||||
|
||||
if install_plugins then
|
||||
packer.sync()
|
||||
end
|
||||
end)
|
||||
|
||||
vim.cmd([[
|
||||
command! NeotestSummary lua require("neotest").summary.toggle()
|
||||
command! NeotestFile lua require("neotest").run.run(vim.fn.expand("%"))
|
||||
command! Neotest lua require("neotest").run.run(vim.fn.getcwd())
|
||||
command! NeotestNearest lua require("neotest").run.run()
|
||||
command! NeotestDebug lua require("neotest").run.run({ strategy = "dap" })
|
||||
command! NeotestAttach lua require("neotest").run.attach()
|
||||
command! NeotestOutput lua require("neotest").output.open()
|
||||
]])
|
||||
```
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
Please provide example test files to reproduce.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Logs**
|
||||
1. Wipe the `neotest.log` file in `stdpath("log")` or `stdpath("data")`.
|
||||
2. Set `log_level = vim.log.levels.DEBUG` in your neotest setup config.
|
||||
3. Reproduce the issue.
|
||||
4. Provide the new logs.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
55
.github/workflows/docgen.yaml
vendored
Normal file
55
.github/workflows/docgen.yaml
vendored
Normal file
|
@ -0,0 +1,55 @@
|
|||
# Taken from telescope
|
||||
name: Generate docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- master
|
||||
pull_request: ~
|
||||
|
||||
jobs:
|
||||
build-sources:
|
||||
name: Generate docs
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-20.04
|
||||
url: https://github.com/neovim/neovim/releases/download/v0.5.1/nvim-linux64.tar.gz
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: date +%F > todays-date
|
||||
- name: Restore cache for today's nightly.
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: _neovim
|
||||
key: ${{ runner.os }}-${{ matrix.url }}-${{ hashFiles('todays-date') }}
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
test -d _neovim || {
|
||||
mkdir -p _neovim
|
||||
curl -sL ${{ matrix.url }} | tar xzf - --strip-components=1 -C "${PWD}/_neovim"
|
||||
}
|
||||
mkdir -p ~/.local/share/nvim/site/pack/vendor/start
|
||||
git clone --depth 1 https://github.com/echasnovski/mini.nvim ~/.local/share/nvim/site/pack/vendor/start/mini.nvim
|
||||
- name: Generating docs
|
||||
run: |
|
||||
export PATH="${PWD}/_neovim/bin:${PATH}"
|
||||
export VIM="${PWD}/_neovim/share/nvim/runtime"
|
||||
nvim --version
|
||||
./scripts/docgen
|
||||
- name: Update documentation
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COMMIT_MSG: |
|
||||
docs: update doc/neotest.txt
|
||||
skip-checks: true
|
||||
run: |
|
||||
git config user.email "actions@github"
|
||||
git config user.name "Github Actions"
|
||||
git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git
|
||||
git add doc/
|
||||
# Only commit and push if we have changes
|
||||
git diff --quiet && git diff --staged --quiet || (git commit -m "${COMMIT_MSG}"; git push origin HEAD:${GITHUB_REF})
|
16
.github/workflows/issues.yaml
vendored
Normal file
16
.github/workflows/issues.yaml
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
name: Add issues to tracking project
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
|
||||
jobs:
|
||||
add-to-project:
|
||||
name: Add issue to project
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/add-to-project@v0.3.0
|
||||
with:
|
||||
project-url: https://github.com/orgs/nvim-neotest/projects/1
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
29
.github/workflows/luarocks-release.yaml
vendored
Normal file
29
.github/workflows/luarocks-release.yaml
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- created
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
pull_request: # Tests the luarocks installation without releasing on PR
|
||||
|
||||
jobs:
|
||||
luarocks-upload:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get Version
|
||||
# tags do not trigger the workflow when they are created by other workflows or releases
|
||||
run: echo "LUAROCKS_VERSION=$(git describe --abbrev=0 --tags)" >> $GITHUB_ENV
|
||||
- name: LuaRocks Upload
|
||||
uses: nvim-neorocks/luarocks-tag-release@v5
|
||||
env:
|
||||
LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }}
|
||||
with:
|
||||
version: ${{ env.LUAROCKS_VERSION }}
|
||||
dependencies: |
|
||||
plenary.nvim
|
77
.github/workflows/workflow.yaml
vendored
Normal file
77
.github/workflows/workflow.yaml
vendored
Normal file
|
@ -0,0 +1,77 @@
|
|||
name: neotest Workflow
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request: ~
|
||||
jobs:
|
||||
style:
|
||||
name: style
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: JohnnyMorganz/stylua-action@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: latest
|
||||
args: --check lua/ tests/
|
||||
|
||||
tests:
|
||||
name: tests
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-22.04
|
||||
rev: nightly/nvim-linux64.tar.gz
|
||||
- os: ubuntu-22.04
|
||||
rev: v0.9.1/nvim-linux64.tar.gz
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: date +%F > todays-date
|
||||
- name: Restore cache for today's nightly.
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: _neovim
|
||||
key: ${{ runner.os }}-${{ matrix.rev }}-${{ hashFiles('todays-date') }}
|
||||
|
||||
- name: Prepare dependencies
|
||||
run: |
|
||||
test -d _neovim || {
|
||||
mkdir -p _neovim
|
||||
curl -sL "https://github.com/neovim/neovim/releases/download/${{ matrix.rev }}" | tar xzf - --strip-components=1 -C "${PWD}/_neovim"
|
||||
}
|
||||
mkdir -p ~/.local/share/nvim/site/pack/vendor/start
|
||||
git clone --depth 1 https://github.com/nvim-lua/plenary.nvim ~/.local/share/nvim/site/pack/vendor/start/plenary.nvim
|
||||
ln -s $(pwd) ~/.local/share/nvim/site/pack/vendor/start
|
||||
export PATH="${PWD}/_neovim/bin:${PATH}"
|
||||
export VIM="${PWD}/_neovim/share/nvim/runtime"
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
export PATH="${PWD}/_neovim/bin:${PATH}"
|
||||
export VIM="${PWD}/_neovim/share/nvim/runtime"
|
||||
nvim --version
|
||||
./scripts/test
|
||||
|
||||
release:
|
||||
name: release
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
needs:
|
||||
- style
|
||||
- tests
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: npx semantic-release
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
neovim/
|
||||
plenary.nvim/
|
||||
doc/tags
|
||||
Session.vim
|
12
.releaserc.json
Normal file
12
.releaserc.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
[
|
||||
"@semantic-release/github",
|
||||
{
|
||||
"successComment": false
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
21
LICENCE.md
Normal file
21
LICENCE.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 Rónán Carrigan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE 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.
|
178
README.md
178
README.md
|
@ -0,0 +1,178 @@
|
|||
# nvim-nio
|
||||
|
||||
A library for asynchronous IO in Neovim, inspired by the asyncio library in Python. The library focuses on providing
|
||||
both common asynchronous primitives and asynchronous APIs for Neovim's core.
|
||||
|
||||
## Motivation
|
||||
|
||||
Work has been ongoing around async libraries in Neovim for years, with a lot of discussion around a [Neovim core
|
||||
implementation](https://github.com/neovim/neovim/issues/19624). A lot of the motivation behind this library can be seen
|
||||
in that discussion.
|
||||
|
||||
nvim-nio aims to provide a simple interface to Lua coroutines that doesn't feel like it gets in the way of your actual
|
||||
logic. You won't even know you're using them. An example of this is error handling. With other libraries, a custom
|
||||
`pcall` or some other custom handling must be used to catch errors. With nvim-nio, Lua's built-in `pcall` works exactly
|
||||
as you'd expect.
|
||||
|
||||
nvim-nio is focused on providing a great developer experience. The API is well documented with examples and full type
|
||||
annotations, which can all be used by the Lua LSP. It's recommended to use
|
||||
[neodev.nvim](https://github.com/folke/neodev.nvim) to get LSP support.
|
||||
|
||||
![image](https://github.com/nvim-lua/plenary.nvim/assets/24252670/0dda462c-0b5c-4300-8e65-b7218e3d2c1e)
|
||||
|
||||
Credit to the async library in [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) and
|
||||
[async.nvim](https://github.com/lewis6991/async.nvim) for inspiring nvim-nio and its implementation.
|
||||
If Neovim core integrates an async library, nvim-nio will aim to maintain compatibility with it if possible.
|
||||
|
||||
## Installation
|
||||
|
||||
Install with your favourite package manager
|
||||
|
||||
[lazy.nvim](https://github.com/folke/lazy.nvim)
|
||||
|
||||
```lua
|
||||
{ "nvim-neotest/nvim-nio" }
|
||||
```
|
||||
|
||||
[dein](https://github.com/Shougo/dein.vim):
|
||||
|
||||
```vim
|
||||
call dein#add("nvim-neotest/nvim-nio")
|
||||
```
|
||||
|
||||
[vim-plug](https://github.com/junegunn/vim-plug)
|
||||
|
||||
```vim
|
||||
Plug 'nvim-neotest/nvim-nio'
|
||||
```
|
||||
|
||||
[packer.nvim](https://github.com/wbthomason/packer.nvim)
|
||||
|
||||
```lua
|
||||
use { "nvim-neotest/nvim-nio" }
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
There are no configuration options currently available.
|
||||
|
||||
## Usage
|
||||
|
||||
nvim-nio is based on the concept of tasks. These tasks represent a series of asynchronous actions that run in a single
|
||||
context. Under the hood, each task is running on a separate lua coroutine.
|
||||
|
||||
Tasks are created by providing an async function to `nio.run`. All async
|
||||
functions must be called from a task.
|
||||
|
||||
```lua
|
||||
local nio = require("nio")
|
||||
|
||||
local task = nio.run(function()
|
||||
nio.sleep(10)
|
||||
print("Hello world")
|
||||
end)
|
||||
```
|
||||
|
||||
For simple use cases tasks won't be too important but they support features such as cancelling and retrieving stack traces.
|
||||
|
||||
nvim-nio comes with built-in modules to help with writing async code. See `:help nvim-nio` for extensive documentation.
|
||||
|
||||
`nio.control`: Primitives for flow control in async functions
|
||||
|
||||
```lua
|
||||
local event = nio.control.event()
|
||||
|
||||
local worker = nio.tasks.run(function()
|
||||
nio.sleep(1000)
|
||||
event.set()
|
||||
end)
|
||||
|
||||
local listeners = {
|
||||
nio.tasks.run(function()
|
||||
event.wait()
|
||||
print("First listener notified")
|
||||
end),
|
||||
nio.tasks.run(function()
|
||||
event.wait()
|
||||
print("Second listener notified")
|
||||
end),
|
||||
}
|
||||
```
|
||||
|
||||
`nio.lsp`: A fully typed and documented async LSP client library, generated from the LSP specification.
|
||||
|
||||
```lua
|
||||
local client = nio.lsp.get_clients({ name = "lua_ls" })[1]
|
||||
|
||||
local err, response = client.request.textDocument_semanticTokens_full({
|
||||
textDocument = { uri = vim.uri_from_bufnr(0) },
|
||||
})
|
||||
|
||||
assert(not err, err)
|
||||
|
||||
for _, token in pairs(response.data) do
|
||||
print(token)
|
||||
end
|
||||
```
|
||||
|
||||
`nio.uv`: Async versions of `vim.loop` functions
|
||||
|
||||
```lua
|
||||
local file_path = "README.md"
|
||||
|
||||
local open_err, file_fd = nio.uv.fs_open(file_path, "r", 438)
|
||||
assert(not open_err, open_err)
|
||||
|
||||
local stat_err, stat = nio.uv.fs_fstat(file_fd)
|
||||
assert(not stat_err, stat_err)
|
||||
|
||||
local read_err, data = nio.uv.fs_read(file_fd, stat.size, 0)
|
||||
assert(not read_err, read_err)
|
||||
|
||||
local close_err = nio.uv.fs_close(file_fd)
|
||||
assert(not close_err, close_err)
|
||||
|
||||
print(data)
|
||||
```
|
||||
|
||||
`nio.ui`: Async versions of vim.ui functions
|
||||
|
||||
```lua
|
||||
local value = nio.ui.input({ prompt = "Enter something: " })
|
||||
print(("You entered: %s"):format(value))
|
||||
```
|
||||
|
||||
`nio.tests`: Async versions of plenary.nvim's test functions
|
||||
|
||||
```lua
|
||||
nio.tests.it("notifies listeners", function()
|
||||
local event = nio.control.event()
|
||||
local notified = 0
|
||||
for _ = 1, 10 do
|
||||
nio.run(function()
|
||||
event.wait()
|
||||
notified = notified + 1
|
||||
end)
|
||||
end
|
||||
|
||||
event.set()
|
||||
nio.sleep(10)
|
||||
assert.equals(10, notified)
|
||||
end)
|
||||
```
|
||||
|
||||
It is also easy to wrap callback style functions to make them asynchronous using `nio.wrap`, which allows easily
|
||||
integrating third-party APIs with nvim-nio.
|
||||
|
||||
```lua
|
||||
local nio = require("nio")
|
||||
|
||||
local sleep = nio.wrap(function(ms, cb)
|
||||
vim.defer_fn(cb, ms)
|
||||
end, 2)
|
||||
|
||||
nio.run(function()
|
||||
sleep(10)
|
||||
print("Slept for 10ms")
|
||||
end)
|
||||
```
|
556
doc/nio.txt
Normal file
556
doc/nio.txt
Normal file
|
@ -0,0 +1,556 @@
|
|||
*nvim-nio.txt* A library for asynchronous IO in Neovim
|
||||
|
||||
==============================================================================
|
||||
nvim-nio *nvim-nio*
|
||||
|
||||
nio....................................................................|nio|
|
||||
nio.control....................................................|nio.control|
|
||||
nio.lsp............................................................|nio.lsp|
|
||||
nio.uv..............................................................|nio.uv|
|
||||
nio.ui..............................................................|nio.ui|
|
||||
nio.tests........................................................|nio.tests|
|
||||
|
||||
|
||||
A library for asynchronous IO in Neovim, inspired by the asyncio library in
|
||||
Python. The library focuses on providing both common asynchronous primitives
|
||||
and asynchronous APIs for Neovim's core.
|
||||
|
||||
nio *nio*
|
||||
|
||||
|
||||
*nio.run()*
|
||||
`run`({func}, {cb})
|
||||
|
||||
Run a function in an async context. This is the entrypoint to all async
|
||||
functionality.
|
||||
>lua
|
||||
local nio = require("nio")
|
||||
nio.run(function()
|
||||
nio.sleep(10)
|
||||
print("Hello world")
|
||||
end)
|
||||
<
|
||||
Parameters~
|
||||
{func} `(function)`
|
||||
{cb?} `(fun(success: boolean,...))` Callback to invoke when the task is
|
||||
complete. If success is false then the parameters will be an error message and
|
||||
a traceback of the error, otherwise it will be the result of the async
|
||||
function.
|
||||
Return~
|
||||
`(nio.tasks.Task)`
|
||||
|
||||
*nio.wrap()*
|
||||
`wrap`({func}, {argc})
|
||||
|
||||
Creates an async function with a callback style function.
|
||||
>lua
|
||||
local nio = require("nio")
|
||||
local sleep = nio.wrap(function(ms, cb)
|
||||
vim.defer_fn(cb, ms)
|
||||
end, 2)
|
||||
|
||||
nio.run(function()
|
||||
sleep(10)
|
||||
print("Slept for 10ms")
|
||||
end)
|
||||
<
|
||||
Parameters~
|
||||
{func} `(function)` A callback style function to be converted. The last
|
||||
argument must be the callback.
|
||||
{argc} `(integer)` The number of arguments of func. Must be included.
|
||||
Return~
|
||||
`(function)` Returns an async function
|
||||
|
||||
*nio.create()*
|
||||
`create`({func}, {argc})
|
||||
|
||||
Takes an async function and returns a function that can run in both async
|
||||
and non async contexts. When running in an async context, the function can
|
||||
return values, but when run in a non-async context, a Task object is
|
||||
returned and an extra callback argument can be supplied to receive the
|
||||
result, with the same signature as the callback for `nio.run`.
|
||||
|
||||
This is useful for APIs where users don't want to create async
|
||||
contexts but which are still used in async contexts internally.
|
||||
Parameters~
|
||||
{func} `(async fun(...))`
|
||||
{argc?} `(integer)` The number of arguments of func. Must be included if there
|
||||
are arguments.
|
||||
|
||||
*nio.gather()*
|
||||
`gather`({functions})
|
||||
|
||||
Run a collection of async functions concurrently and return when
|
||||
all have finished.
|
||||
If any of the functions fail, all pending tasks will be cancelled and the
|
||||
error will be re-raised
|
||||
|
||||
Parameters~
|
||||
{functions} `(function[])`
|
||||
Return~
|
||||
`(any[])` Results of all functions
|
||||
|
||||
*nio.first()*
|
||||
`first`({functions})
|
||||
|
||||
Run a collection of async functions concurrently and return the result of
|
||||
the first to finish.
|
||||
|
||||
Parameters~
|
||||
{functions} `(function[])`
|
||||
Return~
|
||||
`(any)`
|
||||
|
||||
*nio.sleep()*
|
||||
`sleep`({ms})
|
||||
|
||||
Suspend the current task for given time.
|
||||
Parameters~
|
||||
{ms} `(number)` Time in milliseconds
|
||||
|
||||
*nio.scheduler()*
|
||||
`scheduler`()
|
||||
|
||||
Yields to the Neovim scheduler to be able to call the API.
|
||||
|
||||
|
||||
nio.api *nio.api*
|
||||
|
||||
Safely proxies calls to the vim.api module while in an async context.
|
||||
|
||||
nio.fn *nio.fn*
|
||||
|
||||
Safely proxies calls to the vim.fn module while in an async context.
|
||||
|
||||
|
||||
==============================================================================
|
||||
nio.control *nio.control*
|
||||
|
||||
|
||||
Provides primitives for flow control in async functions
|
||||
|
||||
*nio.control.event()*
|
||||
`event`()
|
||||
|
||||
Create a new event
|
||||
|
||||
An event can signal to multiple listeners to resume execution
|
||||
The event can be set from a non-async context.
|
||||
|
||||
>lua
|
||||
local event = nio.control.event()
|
||||
|
||||
local worker = nio.tasks.run(function()
|
||||
nio.sleep(1000)
|
||||
event.set()
|
||||
end)
|
||||
|
||||
local listeners = {
|
||||
nio.tasks.run(function()
|
||||
event.wait()
|
||||
print("First listener notified")
|
||||
end),
|
||||
nio.tasks.run(function()
|
||||
event.wait()
|
||||
print("Second listener notified")
|
||||
end),
|
||||
}
|
||||
<
|
||||
Return~
|
||||
`(nio.control.Event)`
|
||||
|
||||
*nio.control.Event*
|
||||
Fields~
|
||||
{set} `(fun(max_woken?: integer): nil)` Set the event and signal to all (or
|
||||
limited number of) listeners that the event has occurred. If max_woken is
|
||||
provided and there are more listeners then the event is cleared immediately
|
||||
{wait} `(async fun(): nil)` Wait for the event to occur, returning immediately
|
||||
if
|
||||
already set
|
||||
{clear} `(fun(): nil)` Clear the event
|
||||
{is_set} `(fun(): boolean)` Returns true if the event is set
|
||||
|
||||
*nio.control.future()*
|
||||
`future`()
|
||||
|
||||
Create a new future
|
||||
|
||||
An future represents a value that will be available at some point and can be awaited upon.
|
||||
The future result can be set from a non-async context.
|
||||
>lua
|
||||
local future = nio.control.future()
|
||||
|
||||
nio.run(function()
|
||||
nio.sleep(100 * math.random(1, 10))
|
||||
if not future.is_set() then
|
||||
future.set("Success!")
|
||||
end
|
||||
end)
|
||||
nio.run(function()
|
||||
nio.sleep(100 * math.random(1, 10))
|
||||
if not future.is_set() then
|
||||
future.set_error("Failed!")
|
||||
end
|
||||
end)
|
||||
|
||||
local success, value = pcall(future.wait)
|
||||
print(("%s: %s"):format(success, value))
|
||||
<
|
||||
Return~
|
||||
`(nio.control.Future)`
|
||||
|
||||
*nio.control.Future*
|
||||
Fields~
|
||||
{set} `(fun(value): nil)` Set the future value and wake all waiters.
|
||||
{set_error} `(fun(message): nil)` Set the error for this future to raise to
|
||||
waiters
|
||||
{wait} `(async fun(): any)` Wait for the value to be set, returning
|
||||
immediately if already set
|
||||
{is_set} `(fun(): boolean)` Returns true if the future is set
|
||||
|
||||
*nio.control.queue()*
|
||||
`queue`({max_size})
|
||||
|
||||
Create a new FIFO queue with async support.
|
||||
>lua
|
||||
local queue = nio.control.queue()
|
||||
|
||||
local producer = nio.tasks.run(function()
|
||||
for i = 1, 10 do
|
||||
nio.sleep(100)
|
||||
queue.put(i)
|
||||
end
|
||||
queue.put(nil)
|
||||
end)
|
||||
|
||||
while true do
|
||||
local value = queue.get()
|
||||
if value == nil then
|
||||
break
|
||||
end
|
||||
print(value)
|
||||
end
|
||||
print("Done")
|
||||
<
|
||||
Parameters~
|
||||
{max_size?} `(integer)` The maximum number of items in the queue, defaults to
|
||||
no limit
|
||||
Return~
|
||||
`(nio.control.Queue)`
|
||||
|
||||
*nio.control.Queue*
|
||||
Fields~
|
||||
{size} `(fun(): number)` Returns the number of items in the queue
|
||||
{max_size} `(fun(): number|nil)` Returns the maximum number of items in the
|
||||
queue
|
||||
{get} `(async fun(): any)` Get a value from the queue, blocking if the queue
|
||||
is empty
|
||||
{get_nowait} `(fun(): any)` Get a value from the queue, erroring if queue is
|
||||
empty.
|
||||
{put} `(async fun(value: any): nil)` Put a value into the queue
|
||||
{put_nowait} `(fun(value: any): nil)` Put a value into the queue, erroring if
|
||||
queue is full.
|
||||
|
||||
*nio.control.semaphore()*
|
||||
`semaphore`({value})
|
||||
|
||||
Create an async semaphore that allows up to a given number of acquisitions.
|
||||
|
||||
>lua
|
||||
local semaphore = nio.control.semaphore(2)
|
||||
|
||||
local value = 0
|
||||
for _ = 1, 10 do
|
||||
nio.run(function()
|
||||
semaphore.with(function()
|
||||
value = value + 1
|
||||
|
||||
nio.sleep(10)
|
||||
print(value) -- Never more than 2
|
||||
|
||||
value = value - 1
|
||||
end)
|
||||
end)
|
||||
end
|
||||
<
|
||||
Parameters~
|
||||
{value} `(integer)` The number of allowed concurrent acquisitions
|
||||
Return~
|
||||
`(nio.control.Semaphore)`
|
||||
|
||||
*nio.control.Semaphore*
|
||||
Fields~
|
||||
{with} `(async fun(callback: fun(): nil): nil)` Run the callback with the
|
||||
semaphore acquired
|
||||
{acquire} `(async fun(): nil)` Acquire the semaphore
|
||||
{release} `(fun(): nil)` Release the semaphore
|
||||
|
||||
|
||||
==============================================================================
|
||||
nio.lsp *nio.lsp*
|
||||
|
||||
|
||||
*nio.lsp.Client*
|
||||
Fields~
|
||||
{request} `(nio.lsp.RequestClient)` Interface to all requests that can be sent
|
||||
by the client
|
||||
{notify} `(nio.lsp.NotifyClient)` Interface to all notifications that can be
|
||||
sent by the client
|
||||
{server_capabilities} `(nio.lsp.types.ServerCapabilities)`
|
||||
|
||||
*nio.lsp.get_clients()*
|
||||
`get_clients`({filters})
|
||||
|
||||
Get active clients, optionally matching the given filters
|
||||
Equivalent to |vim.lsp.get_clients|
|
||||
Parameters~
|
||||
{filters?} `(nio.lsp.GetClientsFilters)`
|
||||
Return~
|
||||
`(nio.lsp.Client[])`
|
||||
|
||||
*nio.lsp.GetClientsFilters*
|
||||
Fields~
|
||||
{id?} `(integer)`
|
||||
{name?} `(string)`
|
||||
{bufnr?} `(integer)`
|
||||
{method?} `(string)`
|
||||
|
||||
*nio.lsp.client()*
|
||||
`client`({client_id})
|
||||
|
||||
an async client for the given client id
|
||||
Parameters~
|
||||
{client_id} `(integer)`
|
||||
Return~
|
||||
`(nio.lsp.Client)`
|
||||
|
||||
|
||||
==============================================================================
|
||||
nio.uv *nio.uv*
|
||||
|
||||
|
||||
Provides asynchronous versions of vim.loop functions.
|
||||
See corresponding function documentation for parameter and return
|
||||
information.
|
||||
>lua
|
||||
local file_path = "README.md"
|
||||
|
||||
local open_err, file_fd = nio.uv.fs_open(file_path, "r", 438)
|
||||
assert(not open_err, open_err)
|
||||
|
||||
local stat_err, stat = nio.uv.fs_fstat(file_fd)
|
||||
assert(not stat_err, stat_err)
|
||||
|
||||
local read_err, data = nio.uv.fs_read(file_fd, stat.size, 0)
|
||||
assert(not read_err, read_err)
|
||||
|
||||
local close_err = nio.uv.fs_close(file_fd)
|
||||
assert(not close_err, close_err)
|
||||
|
||||
print(data)
|
||||
<
|
||||
|
||||
Fields~
|
||||
{close} `(async fun(handle: nio.uv.Handle))`
|
||||
{fs_open} `(async fun(path: any, flags: any, mode: any):
|
||||
(string|nil,integer|nil))`
|
||||
{fs_read} `(async fun(fd: integer, size: integer, offset?: integer):
|
||||
(string|nil,string|nil))`
|
||||
{fs_close} `(async fun(fd: integer): (string|nil,boolean|nil))`
|
||||
{fs_unlink} `(async fun(path: string): (string|nil,boolean|nil))`
|
||||
{fs_write} `(async fun(fd: any, data: any, offset?: any):
|
||||
(string|nil,integer|nil))`
|
||||
{fs_mkdir} `(async fun(path: string, mode: integer):
|
||||
(string|nil,boolean|nil))`
|
||||
{fs_mkdtemp} `(async fun(template: string): (string|nil,string|nil))`
|
||||
{fs_rmdir} `(async fun(path: string): (string|nil,boolean|nil))`
|
||||
{fs_stat} `(async fun(path: string): (string|nil,nio.uv.Stat|nil))`
|
||||
{fs_fstat} `(async fun(fd: integer): (string|nil,nio.uv.Stat|nil))`
|
||||
{fs_lstat} `(async fun(path: string): (string|nil,nio.uv.Stat|nil))`
|
||||
{fs_statfs} `(async fun(path: string): (string|nil,nio.uv.StatFs|nil))`
|
||||
{fs_rename} `(async fun(old_path: string, new_path: string):
|
||||
(string|nil,boolean|nil))`
|
||||
{fs_fsync} `(async fun(fd: integer): (string|nil,boolean|nil))`
|
||||
{fs_fdatasync} `(async fun(fd: integer): (string|nil,boolean|nil))`
|
||||
{fs_ftruncate} `(async fun(fd: integer, offset: integer):
|
||||
(string|nil,boolean|nil))`
|
||||
{fs_sendfile} `(async fun(out_fd: integer, in_fd: integer, in_offset: integer,
|
||||
length: integer): (string|nil,integer|nil))`
|
||||
{fs_access} `(async fun(path: string, mode: integer):
|
||||
(string|nil,boolean|nil))`
|
||||
{fs_chmod} `(async fun(path: string, mode: integer):
|
||||
(string|nil,boolean|nil))`
|
||||
{fs_fchmod} `(async fun(fd: integer, mode: integer):
|
||||
(string|nil,boolean|nil))`
|
||||
{fs_utime} `(async fun(path: string, atime: number, mtime: number):
|
||||
(string|nil,boolean|nil))`
|
||||
{fs_futime} `(async fun(fd: integer, atime: number, mtime: number):
|
||||
(string|nil,boolean|nil))`
|
||||
{fs_link} `(async fun(path: string, new_path: string):
|
||||
(string|nil,boolean|nil))`
|
||||
{fs_symlink} `(async fun(path: string, new_path: string, flags?: integer):
|
||||
(string|nil,boolean|nil))`
|
||||
{fs_readlink} `(async fun(path: string): (string|nil,string|nil))`
|
||||
{fs_realpath} `(async fun(path: string): (string|nil,string|nil))`
|
||||
{fs_chown} `(async fun(path: string, uid: integer, gid: integer):
|
||||
(string|nil,boolean|nil))`
|
||||
{fs_fchown} `(async fun(fd: integer, uid: integer, gid: integer):
|
||||
(string|nil,boolean|nil))`
|
||||
{fs_lchown} `(async fun(path: string, uid: integer, gid: integer):
|
||||
(string|nil,boolean|nil))`
|
||||
{fs_copyfile} `(async fun(path: any, new_path: any, flags?: any):
|
||||
(string|nil,boolean|nil))`
|
||||
{fs_opendir} `(async fun(path: string, entries?: integer):
|
||||
(string|nil,nio.uv.Dir|nil))`
|
||||
{fs_readdir} `(async fun(dir: nio.uv.Dir):
|
||||
(string|nil,nio.uv.DirEntry[]|nil))`
|
||||
{fs_closedir} `(async fun(dir: nio.uv.Dir): (string|nil,boolean|nil))`
|
||||
{fs_scandir} `(async fun(path: string): (string|nil,nio.uv.DirEntry[]|nil))`
|
||||
{shutdown} `(async fun(stream: nio.uv.Stream): string|nil)`
|
||||
{listen} `(async fun(stream: nio.uv.Stream, backlog: integer): string|nil)`
|
||||
{write} `(async fun(stream: nio.uv.Stream, data: string|string[]):
|
||||
string|nil)`
|
||||
{write2} `(async fun(stream: nio.uv.Stream, data: string|string[],
|
||||
send_handle: nio.uv.Stream): string|nil)`
|
||||
|
||||
*nio.uv.Handle*
|
||||
|
||||
*nio.uv.Stream*
|
||||
Inherits: `nio.uv.Handle`
|
||||
|
||||
|
||||
*nio.uv.Stat*
|
||||
Fields~
|
||||
{dev} `(integer)`
|
||||
{mode} `(integer)`
|
||||
{nlink} `(integer)`
|
||||
{uid} `(integer)`
|
||||
{gid} `(integer)`
|
||||
{rdev} `(integer)`
|
||||
{ino} `(integer)`
|
||||
{size} `(integer)`
|
||||
{blksize} `(integer)`
|
||||
{blocks} `(integer)`
|
||||
{flags} `(integer)`
|
||||
{gen} `(integer)`
|
||||
{atime} `(nio.uv.StatTime)`
|
||||
{mtime} `(nio.uv.StatTime)`
|
||||
{ctime} `(nio.uv.StatTime)`
|
||||
{birthtime} `(nio.uv.StatTime)`
|
||||
{type} `(string)`
|
||||
|
||||
*nio.uv.StatTime*
|
||||
Fields~
|
||||
{sec} `(integer)`
|
||||
{nsec} `(integer)`
|
||||
|
||||
*nio.uv.StatFs*
|
||||
Fields~
|
||||
{type} `(integer)`
|
||||
{bsize} `(integer)`
|
||||
{blocks} `(integer)`
|
||||
{bfree} `(integer)`
|
||||
{bavail} `(integer)`
|
||||
{files} `(integer)`
|
||||
{ffree} `(integer)`
|
||||
|
||||
*nio.uv.Dir*
|
||||
|
||||
*nio.uv.DirEntry*
|
||||
|
||||
|
||||
==============================================================================
|
||||
nio.ui *nio.ui*
|
||||
|
||||
|
||||
Async versions of vim.ui functions.
|
||||
|
||||
*nio.ui.input()*
|
||||
`input`({args})
|
||||
|
||||
Prompt the user for input.
|
||||
See |vim.ui.input()| for details.
|
||||
>lua
|
||||
local value = nio.ui.input({ prompt = "Enter something: " })
|
||||
print(("You entered: %s"):format(value))
|
||||
<
|
||||
|
||||
Parameters~
|
||||
{args} `(nio.ui.InputArgs)`
|
||||
|
||||
*nio.ui.InputArgs*
|
||||
Fields~
|
||||
{prompt} `(string|nil)` Text of the prompt
|
||||
{default} `(string|nil)` Default reply to the input
|
||||
{completion} `(string|nil)` Specifies type of completion supported for input.
|
||||
Supported types are the same that can be supplied to a user-defined command
|
||||
using the "-complete=" argument. See |:command-completion|
|
||||
{highlight} `(function)` Function that will be used for highlighting user
|
||||
inputs.
|
||||
|
||||
*nio.ui.select()*
|
||||
`select`({items}, {args})
|
||||
|
||||
Prompts the user to pick from a list of items
|
||||
See |vim.ui.select()| for details.
|
||||
<
|
||||
local value = nio.ui.select({ "foo", "bar", "baz" }, { prompt = "Select something: " })
|
||||
print(("You entered: %s"):format(value))
|
||||
<
|
||||
|
||||
Parameters~
|
||||
{items} `(any[])`
|
||||
{args} `(nio.ui.SelectArgs)`
|
||||
|
||||
*nio.ui.SelectArgs*
|
||||
Fields~
|
||||
{prompt} `(string|nil)` Text of the prompt. Defaults to `Select one of:`
|
||||
{format_item} `(function|nil)` Function to format an individual item from
|
||||
`items`. Defaults to `tostring`.
|
||||
{kind} `(string|nil)` Arbitrary hint string indicating the item shape. Plugins
|
||||
reimplementing `vim.ui.select` may wish to use this to infer the structure or
|
||||
semantics of `items`, or the context in which select() was called.
|
||||
|
||||
|
||||
==============================================================================
|
||||
nio.tests *nio.tests*
|
||||
|
||||
|
||||
Wrappers around plenary.nvim's test functions for writing async tests
|
||||
>lua
|
||||
a.it("notifies listeners", function()
|
||||
local event = nio.control.event()
|
||||
local notified = 0
|
||||
for _ = 1, 10 do
|
||||
nio.run(function()
|
||||
event.wait()
|
||||
notified = notified + 1
|
||||
end)
|
||||
end
|
||||
|
||||
event.set()
|
||||
nio.sleep(10)
|
||||
assert.equals(10, notified)
|
||||
end)
|
||||
|
||||
*nio.tests.it()*
|
||||
`it`({name}, {async_func})
|
||||
|
||||
Parameters~
|
||||
{name} `(string)`
|
||||
{async_func} `(function)`
|
||||
|
||||
*nio.tests.before_each()*
|
||||
`before_each`({async_func})
|
||||
|
||||
Parameters~
|
||||
{async_func} `(function)`
|
||||
|
||||
*nio.tests.after_each()*
|
||||
`after_each`({async_func})
|
||||
|
||||
Parameters~
|
||||
{async_func} `(function)`
|
||||
|
||||
|
||||
vim:tw=78:ts=8:noet:ft=help:norl:
|
304
lua/nio/control.lua
Normal file
304
lua/nio/control.lua
Normal file
|
@ -0,0 +1,304 @@
|
|||
local tasks = require("nio.tasks")
|
||||
|
||||
local nio = {}
|
||||
|
||||
---@toc_entry nio.control
|
||||
---@text
|
||||
--- Provides primitives for flow control in async functions
|
||||
---@class nio.control
|
||||
nio.control = {}
|
||||
|
||||
--- Create a new event
|
||||
---
|
||||
--- An event can signal to multiple listeners to resume execution
|
||||
--- The event can be set from a non-async context.
|
||||
---
|
||||
--- ```lua
|
||||
--- local event = nio.control.event()
|
||||
---
|
||||
--- local worker = nio.tasks.run(function()
|
||||
--- nio.sleep(1000)
|
||||
--- event.set()
|
||||
--- end)
|
||||
---
|
||||
--- local listeners = {
|
||||
--- nio.tasks.run(function()
|
||||
--- event.wait()
|
||||
--- print("First listener notified")
|
||||
--- end),
|
||||
--- nio.tasks.run(function()
|
||||
--- event.wait()
|
||||
--- print("Second listener notified")
|
||||
--- end),
|
||||
--- }
|
||||
--- ```
|
||||
---@return nio.control.Event
|
||||
function nio.control.event()
|
||||
local waiters = {}
|
||||
local is_set = false
|
||||
return {
|
||||
is_set = function()
|
||||
return is_set
|
||||
end,
|
||||
set = function(max_woken)
|
||||
if is_set then
|
||||
return
|
||||
end
|
||||
is_set = true
|
||||
local waiters_to_notify = {}
|
||||
max_woken = max_woken or #waiters
|
||||
while #waiters > 0 and #waiters_to_notify < max_woken do
|
||||
waiters_to_notify[#waiters_to_notify + 1] = table.remove(waiters)
|
||||
end
|
||||
if #waiters > 0 then
|
||||
is_set = false
|
||||
end
|
||||
for _, waiter in ipairs(waiters_to_notify) do
|
||||
waiter()
|
||||
end
|
||||
end,
|
||||
wait = tasks.wrap(function(callback)
|
||||
if is_set then
|
||||
callback()
|
||||
else
|
||||
waiters[#waiters + 1] = callback
|
||||
end
|
||||
end, 1),
|
||||
clear = function()
|
||||
is_set = false
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
---@class nio.control.Event
|
||||
---@field set fun(max_woken?: integer): nil Set the event and signal to all (or limited number of) listeners that the event has occurred. If max_woken is provided and there are more listeners then the event is cleared immediately
|
||||
---@field wait async fun(): nil Wait for the event to occur, returning immediately if
|
||||
--- already set
|
||||
---@field clear fun(): nil Clear the event
|
||||
---@field is_set fun(): boolean Returns true if the event is set
|
||||
|
||||
--- Create a new future
|
||||
---
|
||||
--- An future represents a value that will be available at some point and can be awaited upon.
|
||||
--- The future result can be set from a non-async context.
|
||||
--- ```lua
|
||||
--- local future = nio.control.future()
|
||||
---
|
||||
--- nio.run(function()
|
||||
--- nio.sleep(100 * math.random(1, 10))
|
||||
--- if not future.is_set() then
|
||||
--- future.set("Success!")
|
||||
--- end
|
||||
--- end)
|
||||
--- nio.run(function()
|
||||
--- nio.sleep(100 * math.random(1, 10))
|
||||
--- if not future.is_set() then
|
||||
--- future.set_error("Failed!")
|
||||
--- end
|
||||
--- end)
|
||||
---
|
||||
--- local success, value = pcall(future.wait)
|
||||
--- print(("%s: %s"):format(success, value))
|
||||
--- ```
|
||||
---@return nio.control.Future
|
||||
function nio.control.future()
|
||||
local waiters = {}
|
||||
local result, err, is_set
|
||||
local wait = tasks.wrap(function(callback)
|
||||
if is_set then
|
||||
callback()
|
||||
else
|
||||
waiters[#waiters + 1] = callback
|
||||
end
|
||||
end, 1)
|
||||
local wake = function()
|
||||
for _, waiter in ipairs(waiters) do
|
||||
waiter()
|
||||
end
|
||||
end
|
||||
return {
|
||||
is_set = function()
|
||||
return is_set
|
||||
end,
|
||||
set = function(value)
|
||||
if is_set then
|
||||
error("Future already set")
|
||||
end
|
||||
result = value
|
||||
is_set = true
|
||||
wake()
|
||||
end,
|
||||
set_error = function(message)
|
||||
if is_set then
|
||||
error("Future already set")
|
||||
end
|
||||
err = message
|
||||
is_set = true
|
||||
wake()
|
||||
end,
|
||||
wait = function()
|
||||
if not is_set then
|
||||
wait()
|
||||
end
|
||||
|
||||
if err then
|
||||
error(err)
|
||||
end
|
||||
return result
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
---@class nio.control.Future
|
||||
---@field set fun(value): nil Set the future value and wake all waiters.
|
||||
---@field set_error fun(message): nil Set the error for this future to raise to
|
||||
---the waiters
|
||||
---@field wait async fun(): any Wait for the value to be set, returning immediately if already set
|
||||
---@field is_set fun(): boolean Returns true if the future is set
|
||||
|
||||
--- Create a new FIFO queue with async support.
|
||||
--- ```lua
|
||||
--- local queue = nio.control.queue()
|
||||
---
|
||||
--- local producer = nio.tasks.run(function()
|
||||
--- for i = 1, 10 do
|
||||
--- nio.sleep(100)
|
||||
--- queue.put(i)
|
||||
--- end
|
||||
--- queue.put(nil)
|
||||
--- end)
|
||||
---
|
||||
--- while true do
|
||||
--- local value = queue.get()
|
||||
--- if value == nil then
|
||||
--- break
|
||||
--- end
|
||||
--- print(value)
|
||||
--- end
|
||||
--- print("Done")
|
||||
--- ```
|
||||
---@param max_size? integer The maximum number of items in the queue, defaults to no limit
|
||||
---@return nio.control.Queue
|
||||
function nio.control.queue(max_size)
|
||||
local items = {}
|
||||
local left_i = 0
|
||||
local right_i = 0
|
||||
local non_empty = nio.control.event()
|
||||
local non_full = nio.control.event()
|
||||
non_full.set()
|
||||
|
||||
local queue = {}
|
||||
|
||||
function queue.size()
|
||||
return right_i - left_i
|
||||
end
|
||||
|
||||
function queue.max_size()
|
||||
return max_size
|
||||
end
|
||||
|
||||
function queue.put(value)
|
||||
non_full.wait()
|
||||
queue.put_nowait(value)
|
||||
end
|
||||
|
||||
function queue.get()
|
||||
non_empty.wait()
|
||||
return queue.get_nowait()
|
||||
end
|
||||
|
||||
function queue.get_nowait()
|
||||
if queue.size() == 0 then
|
||||
error("Queue is empty")
|
||||
end
|
||||
left_i = left_i + 1
|
||||
local item = items[left_i]
|
||||
items[left_i] = nil
|
||||
if left_i == right_i then
|
||||
non_empty.clear()
|
||||
end
|
||||
non_full.set(1)
|
||||
return item
|
||||
end
|
||||
|
||||
function queue.put_nowait(value)
|
||||
if queue.size() == max_size then
|
||||
error("Queue is full")
|
||||
end
|
||||
right_i = right_i + 1
|
||||
items[right_i] = value
|
||||
non_empty.set(1)
|
||||
if queue.size() == max_size then
|
||||
non_full.clear()
|
||||
end
|
||||
end
|
||||
|
||||
return queue
|
||||
end
|
||||
|
||||
---@class nio.control.Queue
|
||||
---@field size fun(): number Returns the number of items in the queue
|
||||
---@field max_size fun(): number|nil Returns the maximum number of items in the queue
|
||||
---@field get async fun(): any Get a value from the queue, blocking if the queue is empty
|
||||
---@field get_nowait fun(): any Get a value from the queue, erroring if queue is empty.
|
||||
---@field put async fun(value: any): nil Put a value into the queue
|
||||
---@field put_nowait fun(value: any): nil Put a value into the queue, erroring if queue is full.
|
||||
|
||||
--- Create an async semaphore that allows up to a given number of acquisitions.
|
||||
---
|
||||
--- ```lua
|
||||
--- local semaphore = nio.control.semaphore(2)
|
||||
---
|
||||
--- local value = 0
|
||||
--- for _ = 1, 10 do
|
||||
--- nio.run(function()
|
||||
--- semaphore.with(function()
|
||||
--- value = value + 1
|
||||
---
|
||||
--- nio.sleep(10)
|
||||
--- print(value) -- Never more than 2
|
||||
---
|
||||
--- value = value - 1
|
||||
--- end)
|
||||
--- end)
|
||||
--- end
|
||||
--- ```
|
||||
---@param value integer The number of allowed concurrent acquisitions
|
||||
---@return nio.control.Semaphore
|
||||
function nio.control.semaphore(value)
|
||||
value = value or 1
|
||||
local released_event = nio.control.event()
|
||||
released_event.set()
|
||||
local acquire = function()
|
||||
released_event.wait()
|
||||
value = value - 1
|
||||
assert(value >= 0, "Semaphore value is negative")
|
||||
if value == 0 then
|
||||
released_event.clear()
|
||||
end
|
||||
end
|
||||
local release = function()
|
||||
value = value + 1
|
||||
released_event.set(1)
|
||||
end
|
||||
return {
|
||||
acquire = acquire,
|
||||
release = release,
|
||||
|
||||
with = function(cb)
|
||||
acquire()
|
||||
local success, err = pcall(cb)
|
||||
release()
|
||||
if not success then
|
||||
error(err)
|
||||
end
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
---@class nio.control.Semaphore
|
||||
---@field with async fun(callback: fun(): nil): nil Run the callback with the semaphore acquired
|
||||
---@field acquire async fun(): nil Acquire the semaphore
|
||||
---@field release fun(): nil Release the semaphore
|
||||
|
||||
return nio.control
|
197
lua/nio/init.lua
Normal file
197
lua/nio/init.lua
Normal file
|
@ -0,0 +1,197 @@
|
|||
local nio = {}
|
||||
local tasks = require("nio.tasks")
|
||||
local control = require("nio.control")
|
||||
local uv = require("nio.uv")
|
||||
local tests = require("nio.tests")
|
||||
local ui = require("nio.ui")
|
||||
local lsp = require("nio.lsp")
|
||||
|
||||
---@tag nvim-nio
|
||||
|
||||
---@toc
|
||||
|
||||
---@text
|
||||
---
|
||||
--- A library for asynchronous IO in Neovim, inspired by the asyncio library in
|
||||
--- Python. The library focuses on providing both common asynchronous primitives
|
||||
--- and asynchronous APIs for Neovim's core.
|
||||
|
||||
---@toc_entry nio
|
||||
---@class nio
|
||||
nio = {}
|
||||
|
||||
nio.control = control
|
||||
nio.uv = uv
|
||||
nio.ui = ui
|
||||
nio.tests = tests
|
||||
nio.tasks = tasks
|
||||
nio.lsp = lsp
|
||||
|
||||
--- Run a function in an async context. This is the entrypoint to all async
|
||||
--- functionality.
|
||||
--- ```lua
|
||||
--- local nio = require("nio")
|
||||
--- nio.run(function()
|
||||
--- nio.sleep(10)
|
||||
--- print("Hello world")
|
||||
--- end)
|
||||
--- ```
|
||||
---@param func function
|
||||
---@param cb? fun(success: boolean,...) Callback to invoke when the task is complete. If success is false then the parameters will be an error message and a traceback of the error, otherwise it will be the result of the async function.
|
||||
---@return nio.tasks.Task
|
||||
function nio.run(func, cb)
|
||||
return tasks.run(func, cb)
|
||||
end
|
||||
|
||||
--- Creates an async function with a callback style function.
|
||||
--- ```lua
|
||||
--- local nio = require("nio")
|
||||
--- local sleep = nio.wrap(function(ms, cb)
|
||||
--- vim.defer_fn(cb, ms)
|
||||
--- end, 2)
|
||||
---
|
||||
--- nio.run(function()
|
||||
--- sleep(10)
|
||||
--- print("Slept for 10ms")
|
||||
--- end)
|
||||
--- ```
|
||||
---@param func function A callback style function to be converted. The last argument must be the callback.
|
||||
---@param argc integer The number of arguments of func. Must be included.
|
||||
---@return function Returns an async function
|
||||
function nio.wrap(func, argc)
|
||||
return tasks.wrap(func, argc)
|
||||
end
|
||||
|
||||
--- Takes an async function and returns a function that can run in both async
|
||||
--- and non async contexts. When running in an async context, the function can
|
||||
--- return values, but when run in a non-async context, a Task object is
|
||||
--- returned and an extra callback argument can be supplied to receive the
|
||||
--- result, with the same signature as the callback for `nio.run`.
|
||||
---
|
||||
--- This is useful for APIs where users don't want to create async
|
||||
--- contexts but which are still used in async contexts internally.
|
||||
---@param func async fun(...)
|
||||
---@param argc? integer The number of arguments of func. Must be included if there are arguments.
|
||||
function nio.create(func, argc)
|
||||
return tasks.create(func, argc)
|
||||
end
|
||||
|
||||
--- Run a collection of async functions concurrently and return when
|
||||
--- all have finished.
|
||||
--- If any of the functions fail, all pending tasks will be cancelled and the
|
||||
--- error will be re-raised
|
||||
---@async
|
||||
---@param functions function[]
|
||||
---@return any[] Results of all functions
|
||||
function nio.gather(functions)
|
||||
local results = {}
|
||||
|
||||
local done_event = control.event()
|
||||
|
||||
local err
|
||||
local running = {}
|
||||
for i, func in ipairs(functions) do
|
||||
local task = tasks.run(func, function(success, ...)
|
||||
if not success then
|
||||
err = ...
|
||||
done_event.set()
|
||||
end
|
||||
results[#results + 1] = { i = i, success = success, result = ... }
|
||||
if #results == #functions then
|
||||
done_event.set()
|
||||
end
|
||||
end)
|
||||
running[#running + 1] = task
|
||||
end
|
||||
done_event.wait()
|
||||
if err then
|
||||
for _, task in ipairs(running) do
|
||||
task.cancel()
|
||||
end
|
||||
error(err)
|
||||
end
|
||||
local sorted = {}
|
||||
for _, result in ipairs(results) do
|
||||
sorted[result.i] = result.result
|
||||
end
|
||||
return sorted
|
||||
end
|
||||
|
||||
--- Run a collection of async functions concurrently and return the result of
|
||||
--- the first to finish.
|
||||
---@async
|
||||
---@param functions function[]
|
||||
---@return any
|
||||
function nio.first(functions)
|
||||
local running_tasks = {}
|
||||
local event = control.event()
|
||||
local failed, result
|
||||
|
||||
for _, func in ipairs(functions) do
|
||||
local task = tasks.run(func, function(success, ...)
|
||||
if event.is_set() then
|
||||
return
|
||||
end
|
||||
failed = not success
|
||||
result = { ... }
|
||||
event.set()
|
||||
end)
|
||||
table.insert(running_tasks, task)
|
||||
end
|
||||
event.wait()
|
||||
for _, task in ipairs(running_tasks) do
|
||||
task.cancel()
|
||||
end
|
||||
if failed then
|
||||
error(unpack(result))
|
||||
end
|
||||
return unpack(result)
|
||||
end
|
||||
|
||||
local async_defer = nio.wrap(function(time, cb)
|
||||
assert(cb, "Cannot call sleep from non-async context")
|
||||
vim.defer_fn(cb, time)
|
||||
end, 2)
|
||||
|
||||
--- Suspend the current task for given time.
|
||||
---@param ms number Time in milliseconds
|
||||
function nio.sleep(ms)
|
||||
async_defer(ms)
|
||||
end
|
||||
|
||||
local wrapped_schedule = nio.wrap(vim.schedule, 1)
|
||||
|
||||
--- Yields to the Neovim scheduler to be able to call the API.
|
||||
---@async
|
||||
function nio.scheduler()
|
||||
wrapped_schedule()
|
||||
end
|
||||
|
||||
---@nodoc
|
||||
local function proxy_vim(prop)
|
||||
return setmetatable({}, {
|
||||
__index = function(_, k)
|
||||
return function(...)
|
||||
-- if we are in a fast event await the scheduler
|
||||
if vim.in_fast_event() then
|
||||
nio.scheduler()
|
||||
end
|
||||
|
||||
return vim[prop][k](...)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
--- Safely proxies calls to the vim.api module while in an async context.
|
||||
nio.api = proxy_vim("api")
|
||||
--- Safely proxies calls to the vim.fn module while in an async context.
|
||||
nio.fn = proxy_vim("fn")
|
||||
|
||||
-- For type checking
|
||||
if false then
|
||||
nio.api = vim.api
|
||||
nio.fn = vim.fn
|
||||
end
|
||||
|
||||
return nio
|
103
lua/nio/logger.lua
Normal file
103
lua/nio/logger.lua
Normal file
|
@ -0,0 +1,103 @@
|
|||
local loggers = {}
|
||||
|
||||
local log_date_format = "%FT%H:%M:%SZ%z"
|
||||
|
||||
---@class nio.Logger
|
||||
---@field trace function
|
||||
---@field debug function
|
||||
---@field info function
|
||||
---@field warn function
|
||||
---@field error function
|
||||
local Logger = {}
|
||||
|
||||
local LARGE = 1e9
|
||||
|
||||
---@return nio.Logger
|
||||
function Logger.new(filename, opts)
|
||||
opts = opts or {}
|
||||
local logger = loggers[filename]
|
||||
if logger then
|
||||
return logger
|
||||
end
|
||||
logger = {}
|
||||
setmetatable(logger, { __index = Logger })
|
||||
loggers[filename] = logger
|
||||
local path_sep = (function()
|
||||
if jit then
|
||||
local os = string.lower(jit.os)
|
||||
if os == "linux" or os == "osx" or os == "bsd" then
|
||||
return "/"
|
||||
else
|
||||
return "\\"
|
||||
end
|
||||
else
|
||||
return package.config:sub(1, 1)
|
||||
end
|
||||
end)()
|
||||
|
||||
local function path_join(...)
|
||||
return table.concat(vim.tbl_flatten({ ... }), path_sep)
|
||||
end
|
||||
|
||||
logger._level = opts.level or vim.log.levels.WARN
|
||||
local ok, logpath = pcall(vim.fn.stdpath, "log")
|
||||
if not ok then
|
||||
logpath = vim.fn.stdpath("cache")
|
||||
end
|
||||
logger._filename = path_join(logpath, filename .. ".log")
|
||||
|
||||
vim.fn.mkdir(logpath, "p")
|
||||
local logfile = assert(io.open(logger._filename, "a+"))
|
||||
|
||||
local log_info = vim.loop.fs_stat(logger._filename)
|
||||
if log_info and log_info.size > LARGE then
|
||||
local warn_msg =
|
||||
string.format("Nio log is large (%d MB): %s", log_info.size / (1000 * 1000), logger._filename)
|
||||
vim.notify(warn_msg, vim.log.levels.WARN)
|
||||
end
|
||||
|
||||
for level, levelnr in pairs(vim.log.levels) do
|
||||
logger[level:lower()] = function(...)
|
||||
local argc = select("#", ...)
|
||||
if levelnr < logger._level then
|
||||
return false
|
||||
end
|
||||
if argc == 0 then
|
||||
return true
|
||||
end
|
||||
local info = debug.getinfo(2, "Sl")
|
||||
local fileinfo = string.format("%s:%s", info.short_src, info.currentline)
|
||||
local parts = {
|
||||
table.concat({ level, "|", os.date(log_date_format), "|", fileinfo, "|" }, " "),
|
||||
}
|
||||
for i = 1, argc do
|
||||
local arg = select(i, ...)
|
||||
if arg == nil then
|
||||
table.insert(parts, "<nil>")
|
||||
elseif type(arg) == "string" then
|
||||
table.insert(parts, arg)
|
||||
elseif type(arg) == "table" and arg.__tostring then
|
||||
table.insert(parts, arg.__tostring(arg))
|
||||
else
|
||||
table.insert(parts, vim.inspect(arg))
|
||||
end
|
||||
end
|
||||
logfile:write(table.concat(parts, " "), "\n")
|
||||
logfile:flush()
|
||||
end
|
||||
end
|
||||
return logger
|
||||
end
|
||||
|
||||
function Logger:set_level(level)
|
||||
self._level = assert(
|
||||
type(level) == "number" and level or vim.log.levels[tostring(level):upper()],
|
||||
string.format("Log level must be one of (trace, debug, info, warn, error), got: %q", level)
|
||||
)
|
||||
end
|
||||
|
||||
function Logger:get_filename()
|
||||
return self._filename
|
||||
end
|
||||
|
||||
return Logger.new("nio")
|
3024
lua/nio/lsp-types.lua
Normal file
3024
lua/nio/lsp-types.lua
Normal file
File diff suppressed because it is too large
Load diff
112
lua/nio/lsp.lua
Normal file
112
lua/nio/lsp.lua
Normal file
|
@ -0,0 +1,112 @@
|
|||
local tasks = require("nio.tasks")
|
||||
local control = require("nio.control")
|
||||
local logger = require("nio.logger")
|
||||
|
||||
local nio = {}
|
||||
|
||||
---@toc_entry nio.lsp
|
||||
---@class nio.lsp
|
||||
nio.lsp = {}
|
||||
|
||||
---@class nio.lsp.Client
|
||||
---@field request nio.lsp.RequestClient Interface to all requests that can be sent by the client
|
||||
---@field notify nio.lsp.NotifyClient Interface to all notifications that can be sent by the client
|
||||
---@field server_capabilities nio.lsp.types.ServerCapabilities
|
||||
|
||||
local async_request = tasks.wrap(function(client, method, params, bufnr, request_id_future, cb)
|
||||
local success, req_id = client.request(method, params, cb, bufnr)
|
||||
if not success then
|
||||
if request_id_future then
|
||||
request_id_future.set_error("Request failed")
|
||||
end
|
||||
error(("Failed to send request. Client %s has shut down"):format(client.id))
|
||||
end
|
||||
if request_id_future then
|
||||
request_id_future.set(req_id)
|
||||
end
|
||||
end, 6)
|
||||
|
||||
--- Get active clients, optionally matching the given filters
|
||||
--- Equivalent to |vim.lsp.get_clients|
|
||||
---@param filters? nio.lsp.GetClientsFilters
|
||||
---@return nio.lsp.Client[]
|
||||
function nio.lsp.get_clients(filters)
|
||||
local clients = {}
|
||||
for _, client in pairs(vim.lsp.get_active_clients(filters)) do
|
||||
clients[#clients + 1] = nio.lsp.client(client.id)
|
||||
end
|
||||
return clients
|
||||
end
|
||||
|
||||
---@class nio.lsp.GetClientsFilters
|
||||
---@field id? integer
|
||||
---@field name? string
|
||||
---@field bufnr? integer
|
||||
---@field method? string
|
||||
|
||||
---Create an async client for the given client id
|
||||
---@param client_id integer
|
||||
---@return nio.lsp.Client
|
||||
function nio.lsp.client(client_id)
|
||||
local n = require("nio")
|
||||
local internal_client =
|
||||
assert(vim.lsp.get_client_by_id(client_id), ("Client not found with ID %s"):format(client_id))
|
||||
|
||||
---@param name string
|
||||
local convert_method = function(name)
|
||||
return name:gsub("__", "$/"):gsub("_", "/")
|
||||
end
|
||||
|
||||
return {
|
||||
server_capabilities = internal_client.server_capabilities,
|
||||
notify = setmetatable({}, {
|
||||
__index = function(_, method)
|
||||
method = convert_method(method)
|
||||
return function(params)
|
||||
return internal_client.notify(method, params)
|
||||
end
|
||||
end,
|
||||
}),
|
||||
request = setmetatable({}, {
|
||||
__index = function(_, method)
|
||||
method = convert_method(method)
|
||||
---@param opts? nio.lsp.RequestOpts
|
||||
return function(params, bufnr, opts)
|
||||
-- No params for this request
|
||||
if type(params) ~= "table" then
|
||||
opts = bufnr
|
||||
bufnr = params
|
||||
end
|
||||
opts = opts or {}
|
||||
local err, result
|
||||
|
||||
local start = vim.loop.now()
|
||||
if opts.timeout then
|
||||
local req_future = control.future()
|
||||
err, result = n.first({
|
||||
function()
|
||||
n.sleep(opts.timeout)
|
||||
local req_id = req_future.wait()
|
||||
n.run(function()
|
||||
async_request(internal_client, "$/cancelRequest", { requestId = req_id }, bufnr)
|
||||
end)
|
||||
return { code = -1, message = "Request timed out" }
|
||||
end,
|
||||
function()
|
||||
return async_request(internal_client, method, params, bufnr, req_future)
|
||||
end,
|
||||
})
|
||||
else
|
||||
err, result = async_request(internal_client, method, params, bufnr)
|
||||
end
|
||||
local elapsed = vim.loop.now() - start
|
||||
logger.trace("Request", method, "took", elapsed, "ms")
|
||||
|
||||
return err, result
|
||||
end
|
||||
end,
|
||||
}),
|
||||
}
|
||||
end
|
||||
|
||||
return nio.lsp
|
212
lua/nio/tasks.lua
Normal file
212
lua/nio/tasks.lua
Normal file
|
@ -0,0 +1,212 @@
|
|||
local nio = {}
|
||||
|
||||
---@class nio.tasks
|
||||
nio.tasks = {}
|
||||
|
||||
---@type table<thread, nio.tasks.Task>
|
||||
---@nodoc
|
||||
local tasks = {}
|
||||
---@type table<nio.tasks.Task, nio.tasks.Task[]>
|
||||
---@nodoc
|
||||
local child_tasks = {}
|
||||
|
||||
-- Coroutine.running() was changed between Lua 5.1 and 5.2:
|
||||
-- - 5.1: Returns the running coroutine, or nil when called by the main thread.
|
||||
-- - 5.2: Returns the running coroutine plus a boolean, true when the running coroutine is the main one.
|
||||
-- For LuaJIT, 5.2 behaviour is enabled with LUAJIT_ENABLE_LUA52COMPAT
|
||||
|
||||
---@nodoc
|
||||
local function current_non_main_co()
|
||||
local data = { coroutine.running() }
|
||||
|
||||
if select("#", unpack(data)) == 2 then
|
||||
local co, is_main = unpack(data)
|
||||
if is_main then
|
||||
return nil
|
||||
end
|
||||
return co
|
||||
end
|
||||
|
||||
return unpack(data)
|
||||
end
|
||||
|
||||
---@text
|
||||
--- Tasks represent a top level running asynchronous function
|
||||
--- Only one task is ever executing at any time.
|
||||
---@class nio.tasks.Task
|
||||
---@field parent? nio.tasks.Task Parent task
|
||||
---@field cancel fun(): nil Cancels the task
|
||||
---@field trace fun(): string Get the stack trace of the task
|
||||
---@field wait async function Wait for the task to finish, returning any result
|
||||
|
||||
---@class nio.tasks.TaskError
|
||||
---@field message string
|
||||
---@field traceback? string
|
||||
|
||||
local format_error = function(message, traceback)
|
||||
if not traceback then
|
||||
return string.format("The coroutine failed with this message: %s", message)
|
||||
end
|
||||
return string.format(
|
||||
"The coroutine failed with this message: %s\n%s",
|
||||
type(message) == "string" and vim.startswith(traceback, message) and ""
|
||||
or ("\n" .. tostring(message)),
|
||||
traceback
|
||||
)
|
||||
end
|
||||
|
||||
---@return nio.tasks.Task
|
||||
---@nodoc
|
||||
function nio.tasks.run(func, cb)
|
||||
local co = coroutine.create(func)
|
||||
local cancelled = false
|
||||
local task = { parent = nio.tasks.current_task() }
|
||||
if task.parent then
|
||||
child_tasks[task.parent] = child_tasks[task.parent] or {}
|
||||
table.insert(child_tasks[task.parent], task)
|
||||
end
|
||||
local future = require("nio").control.future()
|
||||
|
||||
function task.cancel()
|
||||
if coroutine.status(co) == "dead" then
|
||||
return
|
||||
end
|
||||
for _, child in pairs(child_tasks[task] or {}) do
|
||||
child.cancel()
|
||||
end
|
||||
cancelled = true
|
||||
end
|
||||
|
||||
function task.trace()
|
||||
return debug.traceback(co)
|
||||
end
|
||||
|
||||
function task.wait()
|
||||
return future.wait()
|
||||
end
|
||||
|
||||
local function close_task(result, err)
|
||||
tasks[co] = nil
|
||||
if err then
|
||||
future.set_error(err)
|
||||
if cb then
|
||||
cb(false, err)
|
||||
elseif not cancelled then
|
||||
error("Async task failed without callback: " .. err)
|
||||
end
|
||||
else
|
||||
future.set(unpack(result))
|
||||
if cb then
|
||||
cb(true, unpack(result))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
tasks[co] = task
|
||||
|
||||
local function step(...)
|
||||
if cancelled then
|
||||
close_task(nil, format_error("Task was cancelled"))
|
||||
return
|
||||
end
|
||||
|
||||
local yielded = { coroutine.resume(co, ...) }
|
||||
local success = yielded[1]
|
||||
|
||||
if not success then
|
||||
close_task(nil, format_error(yielded[2], debug.traceback(co)))
|
||||
return
|
||||
end
|
||||
|
||||
if coroutine.status(co) == "dead" then
|
||||
close_task({ unpack(yielded, 2, table.maxn(yielded)) })
|
||||
return
|
||||
end
|
||||
|
||||
local _, nargs, err_or_fn = unpack(yielded)
|
||||
|
||||
if type(err_or_fn) ~= "function" then
|
||||
error(
|
||||
("Async internal error: expected function, got %s\nContext: %s\n%s"):format(
|
||||
type(err_or_fn),
|
||||
vim.inspect(yielded),
|
||||
debug.traceback(co)
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
local args = { select(4, unpack(yielded)) }
|
||||
|
||||
args[nargs] = step
|
||||
|
||||
err_or_fn(unpack(args, 1, nargs))
|
||||
end
|
||||
|
||||
step()
|
||||
return task
|
||||
end
|
||||
|
||||
---@param func function
|
||||
---@param argc? number
|
||||
---@return function
|
||||
function nio.tasks.create(func, argc)
|
||||
vim.validate({
|
||||
func = { func, "function" },
|
||||
argc = { argc, "number", true },
|
||||
})
|
||||
argc = argc or 0
|
||||
return function(...)
|
||||
if current_non_main_co() then
|
||||
return func(...)
|
||||
end
|
||||
local args = { ... }
|
||||
local callback
|
||||
if #args > argc then
|
||||
callback = table.remove(args)
|
||||
end
|
||||
return nio.tasks.run(function()
|
||||
func(unpack(args))
|
||||
end, callback)
|
||||
end
|
||||
end
|
||||
|
||||
---@package
|
||||
---@nodoc
|
||||
function nio.tasks.wrap(func, argc)
|
||||
vim.validate({ func = { func, "function" }, argc = { argc, "number" } })
|
||||
local protected = function(...)
|
||||
local args = { ... }
|
||||
local cb = args[argc]
|
||||
args[argc] = function(...)
|
||||
cb(true, ...)
|
||||
end
|
||||
xpcall(func, function(err)
|
||||
cb(false, err, debug.traceback())
|
||||
end, unpack(args, 1, argc))
|
||||
end
|
||||
|
||||
return function(...)
|
||||
if not current_non_main_co() then
|
||||
return func(...)
|
||||
end
|
||||
|
||||
local ret = { coroutine.yield(argc, protected, ...) }
|
||||
local success = ret[1]
|
||||
if not success then
|
||||
error(("Wrapped function failed: %s\n%s"):format(ret[2], ret[3]))
|
||||
end
|
||||
return unpack(ret, 2, table.maxn(ret))
|
||||
end
|
||||
end
|
||||
|
||||
--- Get the current running task
|
||||
---@return nio.tasks.Task|nil
|
||||
function nio.tasks.current_task()
|
||||
local co = current_non_main_co()
|
||||
if not co then
|
||||
return nil
|
||||
end
|
||||
return tasks[co]
|
||||
end
|
||||
|
||||
return nio.tasks
|
64
lua/nio/tests.lua
Normal file
64
lua/nio/tests.lua
Normal file
|
@ -0,0 +1,64 @@
|
|||
local tasks = require("nio.tasks")
|
||||
|
||||
local nio = {}
|
||||
|
||||
---@toc_entry nio.tests
|
||||
---@text
|
||||
--- Wrappers around plenary.nvim's test functions for writing async tests
|
||||
--- ```lua
|
||||
--- a.it("notifies listeners", function()
|
||||
--- local event = nio.control.event()
|
||||
--- local notified = 0
|
||||
--- for _ = 1, 10 do
|
||||
--- nio.run(function()
|
||||
--- event.wait()
|
||||
--- notified = notified + 1
|
||||
--- end)
|
||||
--- end
|
||||
---
|
||||
--- event.set()
|
||||
--- nio.sleep(10)
|
||||
--- assert.equals(10, notified)
|
||||
--- end)
|
||||
---@class nio.tests
|
||||
nio.tests = {}
|
||||
|
||||
local with_timeout = function(func, timeout)
|
||||
local success, err
|
||||
return function()
|
||||
local task = tasks.run(func, function(success_, err_)
|
||||
success = success_
|
||||
if not success_ then
|
||||
err = err_
|
||||
end
|
||||
end)
|
||||
|
||||
vim.wait(timeout or 2000, function()
|
||||
return success ~= nil
|
||||
end, 20, false)
|
||||
|
||||
if success == nil then
|
||||
error(string.format("Test task timed out\n%s", task.trace()))
|
||||
elseif not success then
|
||||
error(string.format("Test task failed with message:\n%s", err))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param name string
|
||||
---@param async_func function
|
||||
nio.tests.it = function(name, async_func)
|
||||
it(name, with_timeout(async_func, tonumber(vim.env.PLENARY_TEST_TIMEOUT)))
|
||||
end
|
||||
|
||||
---@param async_func function
|
||||
nio.tests.before_each = function(async_func)
|
||||
before_each(with_timeout(async_func))
|
||||
end
|
||||
|
||||
---@param async_func function
|
||||
nio.tests.after_each = function(async_func)
|
||||
after_each(with_timeout(async_func))
|
||||
end
|
||||
|
||||
return nio.tests
|
50
lua/nio/ui.lua
Normal file
50
lua/nio/ui.lua
Normal file
|
@ -0,0 +1,50 @@
|
|||
local tasks = require("nio.tasks")
|
||||
|
||||
local nio = {}
|
||||
|
||||
---@toc_entry nio.ui
|
||||
---@text
|
||||
--- Async versions of vim.ui functions.
|
||||
---@class nio.ui
|
||||
nio.ui = {}
|
||||
|
||||
--- Prompt the user for input.
|
||||
--- See |vim.ui.input()| for details.
|
||||
--- ```lua
|
||||
--- local value = nio.ui.input({ prompt = "Enter something: " })
|
||||
--- print(("You entered: %s"):format(value))
|
||||
--- ```
|
||||
---@async
|
||||
---@param args nio.ui.InputArgs
|
||||
function nio.ui.input(args) end
|
||||
|
||||
---@class nio.ui.InputArgs
|
||||
---@field prompt string|nil Text of the prompt
|
||||
---@field default string|nil Default reply to the input
|
||||
---@field completion string|nil Specifies type of completion supported for input. Supported types are the same that can be supplied to a user-defined command using the "-complete=" argument. See |:command-completion|
|
||||
---@field highlight function Function that will be used for highlighting user inputs.
|
||||
|
||||
--- Prompts the user to pick from a list of items
|
||||
--- See |vim.ui.select()| for details.
|
||||
--- ```
|
||||
--- local value = nio.ui.select({ "foo", "bar", "baz" }, { prompt = "Select something: " })
|
||||
--- print(("You entered: %s"):format(value))
|
||||
--- ```
|
||||
---@async
|
||||
---@param items any[]
|
||||
---@param args nio.ui.SelectArgs
|
||||
function nio.ui.select(items, args) end
|
||||
|
||||
---@class nio.ui.SelectArgs
|
||||
---@field prompt string|nil Text of the prompt. Defaults to `Select one of:`
|
||||
---@field format_item function|nil Function to format an individual item from `items`. Defaults to `tostring`.
|
||||
---@field kind string|nil Arbitrary hint string indicating the item shape. Plugins reimplementing `vim.ui.select` may wish to use this to infer the structure or semantics of `items`, or the context in which select() was called.
|
||||
|
||||
nio.ui = {
|
||||
---@nodoc
|
||||
select = tasks.wrap(vim.ui.select, 3),
|
||||
---@nodoc
|
||||
input = tasks.wrap(vim.ui.input, 2),
|
||||
}
|
||||
|
||||
return nio.ui
|
169
lua/nio/uv.lua
Normal file
169
lua/nio/uv.lua
Normal file
|
@ -0,0 +1,169 @@
|
|||
local tasks = require("nio.tasks")
|
||||
|
||||
local nio = {}
|
||||
|
||||
---@toc_entry nio.uv
|
||||
---@text
|
||||
--- Provides asynchronous versions of vim.loop functions.
|
||||
--- See corresponding function documentation for parameter and return
|
||||
--- information.
|
||||
--- ```lua
|
||||
--- local file_path = "README.md"
|
||||
---
|
||||
--- local open_err, file_fd = nio.uv.fs_open(file_path, "r", 438)
|
||||
--- assert(not open_err, open_err)
|
||||
---
|
||||
--- local stat_err, stat = nio.uv.fs_fstat(file_fd)
|
||||
--- assert(not stat_err, stat_err)
|
||||
---
|
||||
--- local read_err, data = nio.uv.fs_read(file_fd, stat.size, 0)
|
||||
--- assert(not read_err, read_err)
|
||||
---
|
||||
--- local close_err = nio.uv.fs_close(file_fd)
|
||||
--- assert(not close_err, close_err)
|
||||
---
|
||||
--- print(data)
|
||||
--- ```
|
||||
---
|
||||
---@class nio.uv
|
||||
---@field close async fun(handle: nio.uv.Handle)
|
||||
---@field fs_open async fun(path: any, flags: any, mode: any): (string|nil,integer|nil)
|
||||
---@field fs_read async fun(fd: integer, size: integer, offset?: integer): (string|nil,string|nil)
|
||||
---@field fs_close async fun(fd: integer): (string|nil,boolean|nil)
|
||||
---@field fs_unlink async fun(path: string): (string|nil,boolean|nil)
|
||||
---@field fs_write async fun(fd: any, data: any, offset?: any): (string|nil,integer|nil)
|
||||
---@field fs_mkdir async fun(path: string, mode: integer): (string|nil,boolean|nil)
|
||||
---@field fs_mkdtemp async fun(template: string): (string|nil,string|nil)
|
||||
---@field fs_rmdir async fun(path: string): (string|nil,boolean|nil)
|
||||
---@field fs_stat async fun(path: string): (string|nil,nio.uv.Stat|nil)
|
||||
---@field fs_fstat async fun(fd: integer): (string|nil,nio.uv.Stat|nil)
|
||||
---@field fs_lstat async fun(path: string): (string|nil,nio.uv.Stat|nil)
|
||||
---@field fs_statfs async fun(path: string): (string|nil,nio.uv.StatFs|nil)
|
||||
---@field fs_rename async fun(old_path: string, new_path: string): (string|nil,boolean|nil)
|
||||
---@field fs_fsync async fun(fd: integer): (string|nil,boolean|nil)
|
||||
---@field fs_fdatasync async fun(fd: integer): (string|nil,boolean|nil)
|
||||
---@field fs_ftruncate async fun(fd: integer, offset: integer): (string|nil,boolean|nil)
|
||||
---@field fs_sendfile async fun(out_fd: integer, in_fd: integer, in_offset: integer, length: integer): (string|nil,integer|nil)
|
||||
---@field fs_access async fun(path: string, mode: integer): (string|nil,boolean|nil)
|
||||
---@field fs_chmod async fun(path: string, mode: integer): (string|nil,boolean|nil)
|
||||
---@field fs_fchmod async fun(fd: integer, mode: integer): (string|nil,boolean|nil)
|
||||
---@field fs_utime async fun(path: string, atime: number, mtime: number): (string|nil,boolean|nil)
|
||||
---@field fs_futime async fun(fd: integer, atime: number, mtime: number): (string|nil,boolean|nil)
|
||||
---@field fs_link async fun(path: string, new_path: string): (string|nil,boolean|nil)
|
||||
---@field fs_symlink async fun(path: string, new_path: string, flags?: integer): (string|nil,boolean|nil)
|
||||
---@field fs_readlink async fun(path: string): (string|nil,string|nil)
|
||||
---@field fs_realpath async fun(path: string): (string|nil,string|nil)
|
||||
---@field fs_chown async fun(path: string, uid: integer, gid: integer): (string|nil,boolean|nil)
|
||||
---@field fs_fchown async fun(fd: integer, uid: integer, gid: integer): (string|nil,boolean|nil)
|
||||
---@field fs_lchown async fun(path: string, uid: integer, gid: integer): (string|nil,boolean|nil)
|
||||
---@field fs_copyfile async fun(path: any, new_path: any, flags?: any): (string|nil,boolean|nil)
|
||||
---@field fs_opendir async fun(path: string, entries?: integer): (string|nil,nio.uv.Dir|nil)
|
||||
---@field fs_readdir async fun(dir: nio.uv.Dir): (string|nil,nio.uv.DirEntry[]|nil)
|
||||
---@field fs_closedir async fun(dir: nio.uv.Dir): (string|nil,boolean|nil)
|
||||
---@field fs_scandir async fun(path: string): (string|nil,nio.uv.DirEntry[]|nil)
|
||||
---@field shutdown async fun(stream: nio.uv.Stream): string|nil
|
||||
---@field listen async fun(stream: nio.uv.Stream, backlog: integer): string|nil
|
||||
---@field write async fun(stream: nio.uv.Stream, data: string|string[]): string|nil
|
||||
---@field write2 async fun(stream: nio.uv.Stream, data: string|string[], send_handle: nio.uv.Stream): string|nil
|
||||
nio.uv = {}
|
||||
|
||||
---@class nio.uv.Handle
|
||||
|
||||
---@class nio.uv.Stream : nio.uv.Handle
|
||||
|
||||
---@class nio.uv.Stat
|
||||
---@field dev integer
|
||||
---@field mode integer
|
||||
---@field nlink integer
|
||||
---@field uid integer
|
||||
---@field gid integer
|
||||
---@field rdev integer
|
||||
---@field ino integer
|
||||
---@field size integer
|
||||
---@field blksize integer
|
||||
---@field blocks integer
|
||||
---@field flags integer
|
||||
---@field gen integer
|
||||
---@field atime nio.uv.StatTime
|
||||
---@field mtime nio.uv.StatTime
|
||||
---@field ctime nio.uv.StatTime
|
||||
---@field birthtime nio.uv.StatTime
|
||||
---@field type string
|
||||
|
||||
---@class nio.uv.StatTime
|
||||
---@field sec integer
|
||||
---@field nsec integer
|
||||
|
||||
---@class nio.uv.StatFs
|
||||
---@field type integer
|
||||
---@field bsize integer
|
||||
---@field blocks integer
|
||||
---@field bfree integer
|
||||
---@field bavail integer
|
||||
---@field files integer
|
||||
---@field ffree integer
|
||||
|
||||
---@class nio.uv.Dir
|
||||
|
||||
---@class nio.uv.DirEntry
|
||||
|
||||
---@nodoc
|
||||
local function add(name, argc)
|
||||
local success, ret = pcall(tasks.wrap, vim.loop[name], argc)
|
||||
|
||||
if not success then
|
||||
error("Failed to add function with name " .. name)
|
||||
end
|
||||
|
||||
nio.uv[name] = ret
|
||||
end
|
||||
|
||||
add("close", 4) -- close a handle
|
||||
-- filesystem operations
|
||||
add("fs_open", 4)
|
||||
add("fs_read", 4)
|
||||
add("fs_close", 2)
|
||||
add("fs_unlink", 2)
|
||||
add("fs_write", 4)
|
||||
add("fs_mkdir", 3)
|
||||
add("fs_mkdtemp", 2)
|
||||
-- 'fs_mkstemp',
|
||||
add("fs_rmdir", 2)
|
||||
add("fs_scandir", 2)
|
||||
add("fs_stat", 2)
|
||||
add("fs_fstat", 2)
|
||||
add("fs_lstat", 2)
|
||||
add("fs_rename", 3)
|
||||
add("fs_fsync", 2)
|
||||
add("fs_fdatasync", 2)
|
||||
add("fs_ftruncate", 3)
|
||||
add("fs_sendfile", 5)
|
||||
add("fs_access", 3)
|
||||
add("fs_chmod", 3)
|
||||
add("fs_fchmod", 3)
|
||||
add("fs_utime", 4)
|
||||
add("fs_futime", 4)
|
||||
-- 'fs_lutime',
|
||||
add("fs_link", 3)
|
||||
add("fs_symlink", 4)
|
||||
add("fs_readlink", 2)
|
||||
add("fs_realpath", 2)
|
||||
add("fs_chown", 4)
|
||||
add("fs_fchown", 4)
|
||||
-- 'fs_lchown',
|
||||
add("fs_copyfile", 4)
|
||||
nio.uv.fs_opendir = tasks.wrap(function(path, entries, cb)
|
||||
vim.loop.fs_opendir(path, cb, entries)
|
||||
end, 3)
|
||||
add("fs_readdir", 2)
|
||||
add("fs_closedir", 2)
|
||||
add("fs_statfs", 2)
|
||||
-- stream
|
||||
add("shutdown", 2)
|
||||
add("listen", 3)
|
||||
-- add('read_start', 2) -- do not do this one, the callback is made multiple times
|
||||
add("write", 3)
|
||||
add("write2", 4)
|
||||
add("shutdown", 2)
|
||||
|
||||
return nio.uv
|
3
scripts/docgen
Executable file
3
scripts/docgen
Executable file
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
|
||||
nvim --headless -c "luafile ./scripts/gendocs.lua" -c 'qa'
|
854
scripts/gendocs.lua
Normal file
854
scripts/gendocs.lua
Normal file
|
@ -0,0 +1,854 @@
|
|||
-- TODO: A lot of this is private code from minidoc, which could be removed if made public
|
||||
|
||||
local minidoc = require("mini.doc")
|
||||
|
||||
local H = {}
|
||||
--stylua: ignore start
|
||||
H.pattern_sets = {
|
||||
-- Patterns for working with afterlines. At the moment deliberately crafted
|
||||
-- to work only on first line without indent.
|
||||
|
||||
-- Determine if line is a function definition. Captures function name and
|
||||
-- arguments. For reference see '2.5.9 – Function Definitions' in Lua manual.
|
||||
afterline_fundef = {
|
||||
'^function%s+(%S-)(%b())', -- Regular definition
|
||||
'^local%s+function%s+(%S-)(%b())', -- Local definition
|
||||
'^(%S+)%s*=%s*function(%b())', -- Regular assignment
|
||||
'^(%S+)%s*=%s*nio.create%(function(%b())', -- Regular assignment
|
||||
'^local%s+(%S+)%s*=%s*function(%b())', -- Local assignment
|
||||
},
|
||||
-- Determine if line is a general assignment
|
||||
afterline_assign = {
|
||||
'^(%S-)%s*=', -- General assignment
|
||||
'^local%s+(%S-)%s*=', -- Local assignment
|
||||
},
|
||||
-- Patterns to work with type descriptions
|
||||
-- (see https://github.com/sumneko/lua-language-server/wiki/EmmyLua-Annotations#types-and-type)
|
||||
types = {
|
||||
'table%b<>',
|
||||
'fun%b(): %S+', 'fun%b()', 'async fun%b(): %S+', 'async fun%b()',
|
||||
'nil', 'any', 'boolean', 'string', 'number', 'integer', 'function', 'table', 'thread', 'userdata', 'lightuserdata',
|
||||
'%.%.%.',
|
||||
"%S+",
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
H.apply_config = function(config)
|
||||
MiniDoc.config = config
|
||||
end
|
||||
|
||||
H.is_disabled = function()
|
||||
return vim.g.minidoc_disable == true or vim.b.minidoc_disable == true
|
||||
end
|
||||
|
||||
H.get_config = function(config)
|
||||
return vim.tbl_deep_extend("force", MiniDoc.config, vim.b.minidoc_config or {}, config or {})
|
||||
end
|
||||
|
||||
-- Work with project specific script ==========================================
|
||||
H.execute_project_script = function(input, output, config)
|
||||
-- Don't process script if there are more than one active `generate` calls
|
||||
if H.generate_is_active then
|
||||
return
|
||||
end
|
||||
|
||||
-- Don't process script if at least one argument is not default
|
||||
if not (input == nil and output == nil and config == nil) then
|
||||
return
|
||||
end
|
||||
|
||||
-- Store information
|
||||
local global_config_cache = vim.deepcopy(MiniDoc.config)
|
||||
local local_config_cache = vim.b.minidoc_config
|
||||
|
||||
-- Pass information to a possible `generate()` call inside script
|
||||
H.generate_is_active = true
|
||||
H.generate_recent_output = nil
|
||||
|
||||
-- Execute script
|
||||
local success = pcall(vim.cmd, "luafile " .. H.get_config(config).script_path)
|
||||
|
||||
-- Restore information
|
||||
MiniDoc.config = global_config_cache
|
||||
vim.b.minidoc_config = local_config_cache
|
||||
H.generate_is_active = nil
|
||||
|
||||
return success
|
||||
end
|
||||
|
||||
-- Default documentation targets ----------------------------------------------
|
||||
H.default_input = function()
|
||||
-- Search in current and recursively in other directories for files with
|
||||
-- 'lua' extension
|
||||
local res = {}
|
||||
for _, dir_glob in ipairs({ ".", "lua/**", "after/**", "colors/**" }) do
|
||||
local files = vim.fn.globpath(dir_glob, "*.lua", false, true)
|
||||
|
||||
-- Use full paths
|
||||
files = vim.tbl_map(function(x)
|
||||
return vim.fn.fnamemodify(x, ":p")
|
||||
end, files)
|
||||
|
||||
-- Put 'init.lua' first among files from same directory
|
||||
table.sort(files, function(a, b)
|
||||
if vim.fn.fnamemodify(a, ":h") == vim.fn.fnamemodify(b, ":h") then
|
||||
if vim.fn.fnamemodify(a, ":t") == "init.lua" then
|
||||
return true
|
||||
end
|
||||
if vim.fn.fnamemodify(b, ":t") == "init.lua" then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return a < b
|
||||
end)
|
||||
table.insert(res, files)
|
||||
end
|
||||
|
||||
return vim.tbl_flatten(res)
|
||||
end
|
||||
|
||||
-- Parsing --------------------------------------------------------------------
|
||||
H.lines_to_block_arr = function(lines, config)
|
||||
local matched_prev, matched_cur
|
||||
|
||||
local res = {}
|
||||
local block_raw = { annotation = {}, section_id = {}, afterlines = {}, line_begin = 1 }
|
||||
|
||||
for i, l in ipairs(lines) do
|
||||
local from, to, section_id = config.annotation_extractor(l)
|
||||
matched_prev, matched_cur = matched_cur, from ~= nil
|
||||
|
||||
if matched_cur then
|
||||
if not matched_prev then
|
||||
-- Finish current block
|
||||
block_raw.line_end = i - 1
|
||||
table.insert(res, H.raw_block_to_block(block_raw, config))
|
||||
|
||||
-- Start new block
|
||||
block_raw = { annotation = {}, section_id = {}, afterlines = {}, line_begin = i }
|
||||
end
|
||||
|
||||
-- Add annotation line without matched annotation pattern
|
||||
table.insert(block_raw.annotation, ("%s%s"):format(l:sub(0, from - 1), l:sub(to + 1)))
|
||||
|
||||
-- Add section id (it is empty string in case of no section id capture)
|
||||
table.insert(block_raw.section_id, section_id or "")
|
||||
else
|
||||
-- Add afterline
|
||||
table.insert(block_raw.afterlines, l)
|
||||
end
|
||||
end
|
||||
block_raw.line_end = #lines
|
||||
table.insert(res, H.raw_block_to_block(block_raw, config))
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
-- Raw block structure is an intermediate step added for convenience. It is
|
||||
-- a table with the following keys:
|
||||
-- - `annotation` - lines (after removing matched annotation pattern) that were
|
||||
-- parsed as annotation.
|
||||
-- - `section_id` - array with length equal to `annotation` length with strings
|
||||
-- captured as section id. Empty string of no section id was captured.
|
||||
-- - Everything else is used as block info (like `afterlines`, etc.).
|
||||
H.raw_block_to_block = function(block_raw, config)
|
||||
if #block_raw.annotation == 0 and #block_raw.afterlines == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
local block = H.new_struct("block", {
|
||||
afterlines = block_raw.afterlines,
|
||||
line_begin = block_raw.line_begin,
|
||||
line_end = block_raw.line_end,
|
||||
})
|
||||
local block_begin = block.info.line_begin
|
||||
|
||||
-- Parse raw block annotation lines from top to bottom. New section starts
|
||||
-- when section id is detected in that line.
|
||||
local section_cur = H.new_struct(
|
||||
"section",
|
||||
{ id = config.default_section_id, line_begin = block_begin }
|
||||
)
|
||||
|
||||
for i, annotation_line in ipairs(block_raw.annotation) do
|
||||
local id = block_raw.section_id[i]
|
||||
if id ~= "" then
|
||||
-- Finish current section
|
||||
if #section_cur > 0 then
|
||||
section_cur.info.line_end = block_begin + i - 2
|
||||
block:insert(section_cur)
|
||||
end
|
||||
|
||||
-- Start new section
|
||||
section_cur = H.new_struct("section", { id = id, line_begin = block_begin + i - 1 })
|
||||
end
|
||||
|
||||
section_cur:insert(annotation_line)
|
||||
end
|
||||
|
||||
if #section_cur > 0 then
|
||||
section_cur.info.line_end = block_begin + #block_raw.annotation - 1
|
||||
block:insert(section_cur)
|
||||
end
|
||||
|
||||
return block
|
||||
end
|
||||
|
||||
-- Hooks ----------------------------------------------------------------------
|
||||
H.apply_structure_hooks = function(doc, hooks)
|
||||
for _, file in ipairs(doc) do
|
||||
for _, block in ipairs(file) do
|
||||
hooks.block_pre(block)
|
||||
|
||||
for _, section in ipairs(block) do
|
||||
hooks.section_pre(section)
|
||||
|
||||
local hook = hooks.sections[section.info.id]
|
||||
if hook ~= nil then
|
||||
hook(section)
|
||||
end
|
||||
|
||||
hooks.section_post(section)
|
||||
end
|
||||
|
||||
hooks.block_post(block)
|
||||
end
|
||||
|
||||
hooks.file(file)
|
||||
end
|
||||
|
||||
hooks.doc(doc)
|
||||
end
|
||||
|
||||
H.alias_register = function(s)
|
||||
if #s == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
-- Remove first word (with bits of surrounding whitespace) while capturing it
|
||||
local alias_name
|
||||
s[1] = s[1]:gsub("%s*(%S+) ?", function(x)
|
||||
alias_name = x
|
||||
return ""
|
||||
end, 1)
|
||||
if alias_name == nil then
|
||||
return
|
||||
end
|
||||
|
||||
MiniDoc.current.aliases = MiniDoc.current.aliases or {}
|
||||
MiniDoc.current.aliases[alias_name] = table.concat(s, "\n")
|
||||
end
|
||||
|
||||
H.alias_replace = function(s)
|
||||
if MiniDoc.current.aliases == nil then
|
||||
return
|
||||
end
|
||||
|
||||
for i, _ in ipairs(s) do
|
||||
for alias_name, alias_desc in pairs(MiniDoc.current.aliases) do
|
||||
-- Escape special characters. This is done here and not while registering
|
||||
-- alias to allow user to refer to aliases by its original name.
|
||||
-- Store escaped words in separate variables because `vim.pesc()` returns
|
||||
-- two values which might conflict if outputs are used as arguments.
|
||||
local name_escaped = vim.pesc(alias_name)
|
||||
local desc_escaped = vim.pesc(alias_desc)
|
||||
s[i] = s[i]:gsub(name_escaped, desc_escaped)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
H.toc_register = function(s)
|
||||
MiniDoc.current.toc = MiniDoc.current.toc or {}
|
||||
table.insert(MiniDoc.current.toc, s)
|
||||
end
|
||||
|
||||
H.toc_insert = function(s)
|
||||
if MiniDoc.current.toc == nil then
|
||||
return
|
||||
end
|
||||
|
||||
-- Render table of contents
|
||||
local toc_lines = {}
|
||||
for _, toc_entry in ipairs(MiniDoc.current.toc) do
|
||||
local _, tag_section = toc_entry.parent:has_descendant(function(x)
|
||||
return type(x) == "table" and x.type == "section" and x.info.id == "@tag"
|
||||
end)
|
||||
tag_section = tag_section or {}
|
||||
|
||||
local lines = {}
|
||||
for i = 1, math.max(#toc_entry, #tag_section) do
|
||||
local left = toc_entry[i] or ""
|
||||
-- Use tag refernce instead of tag enclosure
|
||||
local right = string.match(tag_section[i], "%*.*%*"):gsub("%*", "|")
|
||||
-- local right = vim.trim((tag_section[i] or ""):gsub("%*", "|"))
|
||||
-- Add visual line only at first entry (while not adding trailing space)
|
||||
local filler = i == 1 and "." or (right == "" and "" or " ")
|
||||
-- Make padding of 2 spaces at both left and right
|
||||
local n_filler = math.max(74 - H.visual_text_width(left) - H.visual_text_width(right), 3)
|
||||
table.insert(lines, (" %s%s%s"):format(left, filler:rep(n_filler), right))
|
||||
end
|
||||
|
||||
table.insert(toc_lines, lines)
|
||||
|
||||
-- Don't show `toc_entry` lines in output
|
||||
toc_entry:clear_lines()
|
||||
end
|
||||
|
||||
for _, l in ipairs(vim.tbl_flatten(toc_lines)) do
|
||||
s:insert(l)
|
||||
end
|
||||
end
|
||||
|
||||
H.add_section_heading = function(s, heading)
|
||||
if #s == 0 or s.type ~= "section" then
|
||||
return
|
||||
end
|
||||
|
||||
-- Add heading
|
||||
s:insert(1, ("%s~"):format(heading))
|
||||
end
|
||||
|
||||
H.enclose_var_name = function(s)
|
||||
if #s == 0 or s.type ~= "section" then
|
||||
return
|
||||
end
|
||||
|
||||
s[1] = s[1]:gsub("(%S+)", "{%1}", 1)
|
||||
end
|
||||
|
||||
---@param init number Start of searching for first "type-like" string. It is
|
||||
--- needed to not detect type early. Like in `@param a_function function`.
|
||||
---@private
|
||||
H.enclose_type = function(s, enclosure, init)
|
||||
if #s == 0 or s.type ~= "section" then
|
||||
return
|
||||
end
|
||||
enclosure = enclosure or "`%(%1%)`"
|
||||
init = init or 1
|
||||
|
||||
local cur_type = H.match_first_pattern(s[1], H.pattern_sets["types"], init)
|
||||
if #cur_type == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
-- Add `%S*` to front and back of found pattern to support their combination
|
||||
-- with `|`. Also allows using `[]` and `?` prefixes.
|
||||
local type_pattern = ("(%%S*%s%%S*)"):format(vim.pesc(cur_type[1]))
|
||||
|
||||
-- Avoid replacing possible match before `init`
|
||||
local l_start = s[1]:sub(1, init - 1)
|
||||
local l_end = s[1]:sub(init):gsub(type_pattern, enclosure, 1)
|
||||
s[1] = ("%s%s"):format(l_start, l_end)
|
||||
end
|
||||
|
||||
-- Infer data from afterlines -------------------------------------------------
|
||||
H.infer_header = function(b)
|
||||
local has_signature = b:has_descendant(function(x)
|
||||
return type(x) == "table" and x.type == "section" and x.info.id == "@signature"
|
||||
end)
|
||||
local has_tag = b:has_descendant(function(x)
|
||||
return type(x) == "table" and x.type == "section" and x.info.id == "@tag"
|
||||
end)
|
||||
|
||||
if has_signature and has_tag then
|
||||
return
|
||||
end
|
||||
|
||||
local l_all = table.concat(b.info.afterlines, " ")
|
||||
local tag, signature
|
||||
|
||||
-- Try function definition
|
||||
local fun_capture = H.match_first_pattern(l_all, H.pattern_sets["afterline_fundef"])
|
||||
if #fun_capture > 0 then
|
||||
tag = tag or ("%s()"):format(fun_capture[1])
|
||||
signature = signature or ("%s%s"):format(fun_capture[1], fun_capture[2])
|
||||
end
|
||||
|
||||
-- Try general assignment
|
||||
local assign_capture = H.match_first_pattern(l_all, H.pattern_sets["afterline_assign"])
|
||||
if #assign_capture > 0 then
|
||||
tag = tag or assign_capture[1]
|
||||
signature = signature or assign_capture[1]
|
||||
end
|
||||
|
||||
if tag ~= nil then
|
||||
-- First insert signature (so that it will appear after tag section)
|
||||
if not has_signature then
|
||||
b:insert(1, H.as_struct({ signature }, "section", { id = "@signature" }))
|
||||
end
|
||||
|
||||
-- Insert tag
|
||||
if not has_tag then
|
||||
b:insert(1, H.as_struct({ tag }, "section", { id = "@tag" }))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function H.is_module(name)
|
||||
if string.find(name, "%(") then
|
||||
return false
|
||||
end
|
||||
if string.find(name, "[A-Z]") then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
H.format_signature = function(line)
|
||||
-- Try capture function signature
|
||||
local name, args = line:match("(%S-)(%b())")
|
||||
|
||||
|
||||
-- Otherwise pick first word
|
||||
name = name or line:match("(%S+)")
|
||||
if not args and H.is_module(name) then
|
||||
return ""
|
||||
end
|
||||
local name_elems = vim.split(name, ".", { plain = true })
|
||||
name = name_elems[#name_elems]
|
||||
|
||||
if not name then
|
||||
return ""
|
||||
end
|
||||
|
||||
-- Tidy arguments
|
||||
if args and args ~= "()" then
|
||||
local arg_parts = vim.split(args:sub(2, -2), ",")
|
||||
local arg_list = {}
|
||||
for _, a in ipairs(arg_parts) do
|
||||
-- Enclose argument in `{}` while controlling whitespace
|
||||
table.insert(arg_list, ("{%s}"):format(vim.trim(a)))
|
||||
end
|
||||
args = ("(%s)"):format(table.concat(arg_list, ", "))
|
||||
end
|
||||
|
||||
return ("`%s`%s"):format(name, args or "")
|
||||
end
|
||||
|
||||
-- Work with structures -------------------------------------------------------
|
||||
-- Constructor
|
||||
H.new_struct = function(struct_type, info)
|
||||
local output = {
|
||||
info = info or {},
|
||||
type = struct_type,
|
||||
}
|
||||
|
||||
output.insert = function(self, index, child)
|
||||
-- Allow both `x:insert(child)` and `x:insert(1, child)`
|
||||
if child == nil then
|
||||
child, index = index, #self + 1
|
||||
end
|
||||
|
||||
if type(child) == "table" then
|
||||
child.parent = self
|
||||
child.parent_index = index
|
||||
end
|
||||
|
||||
table.insert(self, index, child)
|
||||
|
||||
H.sync_parent_index(self)
|
||||
end
|
||||
|
||||
output.remove = function(self, index)
|
||||
index = index or #self
|
||||
table.remove(self, index)
|
||||
|
||||
H.sync_parent_index(self)
|
||||
end
|
||||
|
||||
output.has_descendant = function(self, predicate)
|
||||
local bool_res, descendant = false, nil
|
||||
H.apply_recursively(function(x)
|
||||
if not bool_res and predicate(x) then
|
||||
bool_res = true
|
||||
descendant = x
|
||||
end
|
||||
end, self)
|
||||
return bool_res, descendant
|
||||
end
|
||||
|
||||
output.has_lines = function(self)
|
||||
return self:has_descendant(function(x)
|
||||
return type(x) == "string"
|
||||
end)
|
||||
end
|
||||
|
||||
output.clear_lines = function(self)
|
||||
for i, x in ipairs(self) do
|
||||
if type(x) == "string" then
|
||||
self[i] = nil
|
||||
else
|
||||
x:clear_lines()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return output
|
||||
end
|
||||
|
||||
H.sync_parent_index = function(x)
|
||||
for i, _ in ipairs(x) do
|
||||
if type(x[i]) == "table" then
|
||||
x[i].parent_index = i
|
||||
end
|
||||
end
|
||||
return x
|
||||
end
|
||||
|
||||
-- Converter (this ensures that children have proper parent-related data)
|
||||
H.as_struct = function(array, struct_type, info)
|
||||
-- Make default info `info` for cases when structure is created manually
|
||||
local default_info = ({
|
||||
section = { id = "@text", line_begin = -1, line_end = -1 },
|
||||
block = { afterlines = {}, line_begin = -1, line_end = -1 },
|
||||
file = { path = "" },
|
||||
doc = { input = {}, output = "", config = H.get_config() },
|
||||
})[struct_type]
|
||||
info = vim.tbl_deep_extend("force", default_info, info or {})
|
||||
|
||||
local res = H.new_struct(struct_type, info)
|
||||
for _, x in ipairs(array) do
|
||||
res:insert(x)
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
-- Work with text -------------------------------------------------------------
|
||||
H.ensure_indent = function(text, n_indent_target)
|
||||
local lines = vim.split(text, "\n")
|
||||
local n_indent, n_indent_cur = math.huge, math.huge
|
||||
|
||||
-- Find number of characters in indent
|
||||
for _, l in ipairs(lines) do
|
||||
-- Update lines indent: minimum of all indents except empty lines
|
||||
if n_indent > 0 then
|
||||
_, n_indent_cur = l:find("^%s*")
|
||||
-- Condition "current n-indent equals line length" detects empty line
|
||||
if (n_indent_cur < n_indent) and (n_indent_cur < l:len()) then
|
||||
n_indent = n_indent_cur
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Ensure indent
|
||||
local indent = string.rep(" ", n_indent_target)
|
||||
for i, l in ipairs(lines) do
|
||||
if l ~= "" then
|
||||
lines[i] = indent .. l:sub(n_indent + 1)
|
||||
end
|
||||
end
|
||||
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
H.align_text = function(text, width, direction)
|
||||
if type(text) ~= "string" then
|
||||
return
|
||||
end
|
||||
text = vim.trim(text)
|
||||
width = width or 78
|
||||
direction = direction or "left"
|
||||
|
||||
-- Don't do anything if aligning left or line is a whitespace
|
||||
if direction == "left" or text:find("^%s*$") then
|
||||
return text
|
||||
end
|
||||
|
||||
local n_left = math.max(0, 78 - H.visual_text_width(text))
|
||||
if direction == "center" then
|
||||
n_left = math.floor(0.5 * n_left)
|
||||
end
|
||||
|
||||
return (" "):rep(n_left) .. text
|
||||
end
|
||||
|
||||
H.visual_text_width = function(text)
|
||||
-- Ignore concealed characters (usually "invisible" in 'help' filetype)
|
||||
local _, n_concealed_chars = text:gsub("([*|`])", "%1")
|
||||
return vim.fn.strdisplaywidth(text) - n_concealed_chars
|
||||
end
|
||||
|
||||
--- Return earliest match among many patterns
|
||||
---
|
||||
--- Logic here is to test among several patterns. If several got a match,
|
||||
--- return one with earliest match.
|
||||
---
|
||||
---@private
|
||||
H.match_first_pattern = function(text, pattern_set, init)
|
||||
local start_tbl = vim.tbl_map(function(pattern)
|
||||
return text:find(pattern, init) or math.huge
|
||||
end, pattern_set)
|
||||
|
||||
local min_start, min_id = math.huge, nil
|
||||
for id, st in ipairs(start_tbl) do
|
||||
if st < min_start then
|
||||
min_start, min_id = st, id
|
||||
end
|
||||
end
|
||||
|
||||
if min_id == nil then
|
||||
return {}
|
||||
end
|
||||
return { text:match(pattern_set[min_id], init) }
|
||||
end
|
||||
|
||||
-- Utilities ------------------------------------------------------------------
|
||||
H.apply_recursively = function(f, x, used)
|
||||
used = used or {}
|
||||
if used[x] then
|
||||
return
|
||||
end
|
||||
f(x)
|
||||
used[x] = true
|
||||
|
||||
if type(x) == "table" then
|
||||
for _, t in ipairs(x) do
|
||||
H.apply_recursively(f, t, used)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
H.collect_strings = function(x)
|
||||
local res = {}
|
||||
H.apply_recursively(function(y)
|
||||
if type(y) == "string" then
|
||||
-- Allow `\n` in strings
|
||||
table.insert(res, vim.split(y, "\n"))
|
||||
end
|
||||
end, x)
|
||||
-- Flatten to only have strings and not table of strings (from `vim.split`)
|
||||
return vim.tbl_flatten(res)
|
||||
end
|
||||
|
||||
H.file_read = function(path)
|
||||
local file = assert(io.open(path))
|
||||
local contents = file:read("*all")
|
||||
file:close()
|
||||
|
||||
return vim.split(contents, "\n")
|
||||
end
|
||||
|
||||
H.file_write = function(path, lines)
|
||||
-- Ensure target directory exists
|
||||
local dir = vim.fn.fnamemodify(path, ":h")
|
||||
vim.fn.mkdir(dir, "p")
|
||||
|
||||
-- Write to file
|
||||
vim.fn.writefile(lines, path, "b")
|
||||
end
|
||||
|
||||
H.full_path = function(path)
|
||||
return vim.fn.resolve(vim.fn.fnamemodify(path, ":p"))
|
||||
end
|
||||
|
||||
H.message = function(msg)
|
||||
vim.cmd("echomsg " .. vim.inspect("(mini.doc) " .. msg))
|
||||
end
|
||||
|
||||
local function wrap(str, limit, indent, indent1)
|
||||
indent = indent or ""
|
||||
indent1 = indent1 or indent
|
||||
limit = limit or 79
|
||||
local here = 1 - #indent1
|
||||
local wrapped = indent1
|
||||
.. str:gsub("(%s+)()(%S+)()", function(sp, st, word, fi)
|
||||
local delta = 0
|
||||
word:gsub("@([@%a])", function(c)
|
||||
if c == "@" then
|
||||
delta = delta + 1
|
||||
elseif c == "x" then
|
||||
delta = delta + 5
|
||||
else
|
||||
delta = delta + 2
|
||||
end
|
||||
end)
|
||||
here = here + delta
|
||||
if fi - here > limit then
|
||||
here = st - #indent + delta
|
||||
return "\n" .. indent .. word
|
||||
end
|
||||
end)
|
||||
|
||||
return vim.split(wrapped, "\n")
|
||||
end
|
||||
|
||||
|
||||
local function create_config(module, header)
|
||||
return {
|
||||
hooks = vim.tbl_extend("force", minidoc.default_hooks, {
|
||||
block_pre = function(b)
|
||||
-- Infer metadata based on afterlines
|
||||
if b:has_lines() and #b.info.afterlines > 0 then H.infer_header(b) end
|
||||
end,
|
||||
section_post = function(section)
|
||||
for i, line in ipairs(section) do
|
||||
if type(line) == "string" then
|
||||
if string.find(line, "^```") then
|
||||
string.gsub(line, "```(.*)", function(lang)
|
||||
section[i] = lang == "" and "<" or (">%s"):format(lang)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
block_post = function(b)
|
||||
if not b:has_lines() then return end
|
||||
|
||||
local found_param, found_field = false, false
|
||||
local n_tag_sections = 0
|
||||
H.apply_recursively(function(x)
|
||||
if not (type(x) == 'table' and x.type == 'section') then return end
|
||||
|
||||
-- Add headings before first occurence of a section which type usually
|
||||
-- appear several times
|
||||
if not found_param and x.info.id == '@param' then
|
||||
H.add_section_heading(x, 'Parameters')
|
||||
found_param = true
|
||||
end
|
||||
if not found_field and x.info.id == '@field' then
|
||||
H.add_section_heading(x, 'Fields')
|
||||
found_field = true
|
||||
end
|
||||
|
||||
if x.info.id == '@tag' then
|
||||
local text = x[1]
|
||||
local tag = string.match(text, "%*.*%*")
|
||||
local prefix = (string.sub(tag, 2, #tag - 1))
|
||||
if not H.is_module(prefix) then
|
||||
prefix = ""
|
||||
end
|
||||
local n_filler = math.max(78 - H.visual_text_width(prefix) - H.visual_text_width(tag), 3)
|
||||
local line = ("%s%s%s"):format(prefix, (" "):rep(n_filler), tag)
|
||||
x:remove(1)
|
||||
x:insert(1, line)
|
||||
x.parent:remove(x.parent_index)
|
||||
n_tag_sections = n_tag_sections + 1
|
||||
x.parent:insert(n_tag_sections, x)
|
||||
end
|
||||
end, b)
|
||||
|
||||
-- b:insert(1, H.as_struct({ string.rep('=', 78) }, 'section'))
|
||||
b:insert(H.as_struct({ '' }, 'section'))
|
||||
end,
|
||||
doc = function(d)
|
||||
-- Render table of contents
|
||||
H.apply_recursively(function(x)
|
||||
if not (type(x) == 'table' and x.type == 'section' and x.info.id == '@toc') then return end
|
||||
H.toc_insert(x)
|
||||
end, d)
|
||||
|
||||
-- Insert modeline
|
||||
d:insert(
|
||||
H.as_struct(
|
||||
{ H.as_struct({ H.as_struct({ ' vim:tw=78:ts=8:noet:ft=help:norl:' }, 'section') }, 'block') },
|
||||
'file'
|
||||
)
|
||||
)
|
||||
end,
|
||||
sections = {
|
||||
['@generic'] = function(s)
|
||||
s:remove(1)
|
||||
end,
|
||||
['@field'] = function(s)
|
||||
-- H.mark_optional(s)
|
||||
if string.find(s[1], "^private ") then
|
||||
s:remove(1)
|
||||
return
|
||||
end
|
||||
H.enclose_var_name(s)
|
||||
H.enclose_type(s, '`%(%1%)`', s[1]:find('%s'))
|
||||
local wrapped = wrap(s[1], 78, "")
|
||||
s:remove(1)
|
||||
for i, line in ipairs(wrapped) do
|
||||
s:insert(i, line)
|
||||
end
|
||||
end,
|
||||
['@alias'] = function(s)
|
||||
local name = s[1]:match('%s*(%S*)')
|
||||
local alias = s[1]:match('%s(.*)$')
|
||||
s[1] = ("`%s` → `%s`"):format(name, alias)
|
||||
H.add_section_heading(s, 'Alias')
|
||||
s:insert(1, H.as_struct({ ("*%s*"):format(name) }, "section", { id = "@tag" }))
|
||||
end,
|
||||
['@param'] = function(s)
|
||||
H.enclose_var_name(s)
|
||||
H.enclose_type(s, '`%(%1%)`', s[1]:find('%s'))
|
||||
local wrapped = wrap(s[1], 78, "")
|
||||
s:remove(1)
|
||||
for i, line in ipairs(wrapped) do
|
||||
s:insert(i, line)
|
||||
end
|
||||
end,
|
||||
['@return'] = function(s)
|
||||
H.enclose_type(s, '`%(%1%)`', 1)
|
||||
H.add_section_heading(s, 'Return')
|
||||
end,
|
||||
['@nodoc'] = function(s) s.parent:clear_lines() end,
|
||||
['@class'] = function(s)
|
||||
H.enclose_var_name(s)
|
||||
-- Add heading
|
||||
local line = s[1]
|
||||
s:remove(1)
|
||||
local class_name = string.match(line, "%{(.*)%}")
|
||||
local inherits = string.match(line, ": (.*)")
|
||||
if inherits then
|
||||
s:insert(1, ("Inherits: `%s`"):format(inherits))
|
||||
s:insert(2, "")
|
||||
end
|
||||
s:insert(1, H.as_struct({ ("*%s*"):format(class_name) }, "section", { id = "@tag" }))
|
||||
end,
|
||||
['@signature'] = function(s)
|
||||
s[1] = H.format_signature(s[1])
|
||||
if s[1] ~= "" then
|
||||
table.insert(s, "")
|
||||
end
|
||||
end,
|
||||
},
|
||||
file = function(f)
|
||||
if not f:has_lines() then
|
||||
return
|
||||
end
|
||||
|
||||
if f.info.path ~= "./lua/" .. module .. "/init.lua" then
|
||||
f:insert(1, H.as_struct({ H.as_struct({ string.rep("=", 78) }, "section") }, "block"))
|
||||
f:insert(H.as_struct({ H.as_struct({ "" }, "section") }, "block"))
|
||||
else
|
||||
f:insert(
|
||||
1,
|
||||
H.as_struct(
|
||||
{
|
||||
H.as_struct(
|
||||
{ header },
|
||||
"section"
|
||||
),
|
||||
},
|
||||
"block"
|
||||
)
|
||||
)
|
||||
f:insert(2, H.as_struct({ H.as_struct({ "" }, "section") }, "block"))
|
||||
f:insert(3, H.as_struct({ H.as_struct({ string.rep("=", 78) }, "section") }, "block"))
|
||||
f:insert(H.as_struct({ H.as_struct({ "" }, "section") }, "block"))
|
||||
end
|
||||
end,
|
||||
}),
|
||||
}
|
||||
end
|
||||
|
||||
minidoc.setup({})
|
||||
minidoc.generate(
|
||||
{
|
||||
"./lua/nio/init.lua",
|
||||
"./lua/nio/control.lua",
|
||||
"./lua/nio/lsp.lua",
|
||||
"./lua/nio/uv.lua",
|
||||
"./lua/nio/ui.lua",
|
||||
"./lua/nio/tests.lua",
|
||||
|
||||
},
|
||||
"doc/nio.txt",
|
||||
create_config("nio", "*nvim-nio.txt* A library for asynchronous IO in Neovim")
|
||||
|
||||
)
|
535
scripts/generate_lsp_types.lua
Normal file
535
scripts/generate_lsp_types.lua
Normal file
|
@ -0,0 +1,535 @@
|
|||
---@class Model
|
||||
---@field requests Request[]
|
||||
---@field notifications Notification[]
|
||||
---@field enumerations Enumeration
|
||||
---@field typeAliases TypeAlias[]
|
||||
---@field structures Structure[]
|
||||
|
||||
---@alias BaseTypes "URI" | "DocumentUri" | "integer" | "uinteger" | "decimal" | "RegExp" | "string" | "boolean" | "null"
|
||||
|
||||
---@class BooleanLiteralType
|
||||
---@field kind "boolean"
|
||||
---@field value boolean
|
||||
|
||||
---@class EnumerationEntry
|
||||
---@field documentation? string An optional documentation.
|
||||
---@field name string The name of the enum item.
|
||||
---@field proposed? boolean Whether this is a proposed enumeration entry. If omitted, the enumeration entry is final.
|
||||
---@field since? string Since when (release number) this enumeration entry is available. Is undefined if not known.
|
||||
---@field value string | float The value.
|
||||
|
||||
---@alias Name "string" | "integer" | "uinteger"
|
||||
|
||||
---@class EnumerationType
|
||||
---@field kind "enumeration"
|
||||
---@field name Name
|
||||
|
||||
---@class IntegerLiteralType
|
||||
---@field kind "integer" Represents an integer literal type (e.g. `kind: 1`).
|
||||
---@field value float
|
||||
|
||||
---@alias Name1 "URI" | "DocumentUri" | "string" | "integer"
|
||||
|
||||
---@class MapKeyTypeItem
|
||||
---@field kind TypeKind
|
||||
---@field name Name1
|
||||
|
||||
---@alias MessageDirection "clientToServer" | "serverToClient" | "both"
|
||||
|
||||
---@class MetaData
|
||||
---@field version string The protocol version.
|
||||
|
||||
---@class ReferenceType
|
||||
---@field kind "reference"
|
||||
---@field name string
|
||||
|
||||
---@class StringLiteralType
|
||||
---@field kind "stringLiteral"
|
||||
---@field value string
|
||||
|
||||
---@alias TypeKind "base" | "reference" | "array" | "map" | "and" | "or" | "tuple" | "literal" | "stringLiteral" | "integerLiteral" | "booleanLiteral"
|
||||
|
||||
---@class BaseType
|
||||
---@field kind "base"
|
||||
---@field name BaseTypes
|
||||
|
||||
---@class Enumeration
|
||||
---@field documentation? string An optional documentation.
|
||||
---@field name string The name of the enumeration.
|
||||
---@field proposed? boolean Whether this is a proposed enumeration. If omitted, the enumeration is final.
|
||||
---@field since? string Since when (release number) this enumeration is available. Is undefined if not known.
|
||||
---@field supportsCustomValues? boolean Whether the enumeration supports custom values (e.g. values which are not part of the set defined in `values`). If omitted no custom values are supported.
|
||||
---@field type EnumerationType The type of the elements.
|
||||
---@field values EnumerationEntry[] The enum values.
|
||||
|
||||
---- Represents a type that can be used as a key in a map type. If a reference type is used then the type must either resolve to a `string` or `integer` type. (e.g. `type ChangeAnnotationIdentifier === string`).
|
||||
---@alias MapKeyType MapKeyTypeItem | ReferenceType
|
||||
|
||||
---@class AndType
|
||||
---@field items Type[]
|
||||
---@field kind "and"
|
||||
|
||||
---@class ArrayType
|
||||
---@field element Type
|
||||
---@field kind "array"
|
||||
|
||||
---@class MapType
|
||||
---@field key MapKeyType
|
||||
---@field kind "map"
|
||||
---@field value Type
|
||||
|
||||
---@class MetaModel
|
||||
---@field enumerations Enumeration[] The enumerations.
|
||||
---@field metaData MetaData Additional meta data.
|
||||
---@field notifications Notification[] The notifications.
|
||||
---@field requests Request[] The requests.
|
||||
---@field structures Structure[] The structures.
|
||||
---@field typeAliases TypeAlias[] The type aliases.
|
||||
|
||||
---@class Notification
|
||||
---@field documentation? string An optional documentation;
|
||||
---@field messageDirection MessageDirection The direction in which this notification is sent in the protocol.
|
||||
---@field method string The request's method name.
|
||||
---@field params? Type | Type[] The parameter type(s) if any.
|
||||
---@field proposed? boolean Whether this is a proposed notification. If omitted the notification is final.
|
||||
---@field registrationMethod? string Optional a dynamic registration method if it different from the request's method.
|
||||
---@field registrationOptions? Type Optional registration options if the notification supports dynamic registration.
|
||||
---@field since? string Since when (release number) this notification is available. Is undefined if not known.
|
||||
|
||||
---@class OrType
|
||||
---@field items Type[]
|
||||
---@field kind "or"
|
||||
|
||||
---@class Property
|
||||
---@field documentation? string An optional documentation.
|
||||
---@field name string The property name;
|
||||
---@field optional? boolean Whether the property is optional. If omitted, the property is mandatory.
|
||||
---@field proposed? boolean Whether this is a proposed property. If omitted, the structure is final.
|
||||
---@field since? string Since when (release number) this property is available. Is undefined if not known.
|
||||
---@field type Type The type of the property
|
||||
|
||||
---@class Request
|
||||
---@field documentation? string An optional documentation;
|
||||
---@field errorData? Type An optional error data type.
|
||||
---@field messageDirection MessageDirection The direction in which this request is sent in the protocol.
|
||||
---@field method string The request's method name.
|
||||
---@field params? Type | Type[] The parameter type(s) if any.
|
||||
---@field partialResult? Type Optional partial result type if the request supports partial result reporting.
|
||||
---@field proposed? boolean Whether this is a proposed feature. If omitted the feature is final.
|
||||
---@field registrationMethod? string Optional a dynamic registration method if it different from the request's method.
|
||||
---@field registrationOptions? Type Optional registration options if the request supports dynamic registration.
|
||||
---@field result Type The result type.
|
||||
---@field since? string Since when (release number) this request is available. Is undefined if not known.
|
||||
|
||||
---@class Structure
|
||||
---@field documentation? string An optional documentation;
|
||||
---@field extends? Type[] Structures extended from. This structures form a polymorphic type hierarchy.
|
||||
---@field mixins? Type[] Structures to mix in. The properties of these structures are `copied` into this structure. Mixins don't form a polymorphic type hierarchy in LSP.
|
||||
---@field name string The name of the structure.
|
||||
---@field properties Property[] The properties.
|
||||
---@field proposed? boolean Whether this is a proposed structure. If omitted, the structure is final.
|
||||
---@field since? string Since when (release number) this structure is available. Is undefined if not known.
|
||||
|
||||
---@class StructureLiteral
|
||||
---@field documentation? string An optional documentation.
|
||||
---@field properties Property[] The properties.
|
||||
---@field proposed? boolean Whether this is a proposed structure. If omitted, the structure is final.
|
||||
---@field since? string Since when (release number) this structure is available. Is undefined if not known.
|
||||
|
||||
---@class StructureLiteralType
|
||||
---@field kind "literal"
|
||||
---@field value StructureLiteral
|
||||
|
||||
---@class TupleType
|
||||
---@field items Type[]
|
||||
---@field kind "tuple"
|
||||
|
||||
---@alias Type BaseType | ReferenceType | ArrayType | MapType | AndType | OrType | TupleType | StructureLiteralType | StringLiteralType | IntegerLiteralType | BooleanLiteralType
|
||||
|
||||
---@class TypeAlias
|
||||
---@field documentation? string An optional documentation.
|
||||
---@field name string The name of the type alias.
|
||||
---@field proposed? boolean Whether this is a proposed type alias. If omitted, the type alias is final.
|
||||
---@field since? string Since when (release number) this structure is available. Is undefined if not known.
|
||||
---@field type Type The aliased type.
|
||||
|
||||
---@class Generator
|
||||
---@field known_objs table<string, Structure | TypeAlias>
|
||||
---@field known_literals table<StructureLiteral, Structure>
|
||||
---@field model Model
|
||||
local Generator = {}
|
||||
Generator.__index = Generator
|
||||
|
||||
function Generator.new(model)
|
||||
local self = setmetatable({}, Generator)
|
||||
self.model = model
|
||||
self.known_objs = {}
|
||||
self.known_literals = {}
|
||||
return self
|
||||
end
|
||||
|
||||
---@param obj Structure | TypeAlias | StructureLiteral
|
||||
---@return Structure | TypeAlias
|
||||
function Generator:register(obj)
|
||||
if not obj.name then
|
||||
if not self.known_literals[obj] then
|
||||
self.known_literals[obj] = {
|
||||
name = ("Structure%s"):format(#vim.tbl_keys(self.known_literals)),
|
||||
documentation = obj.documentation,
|
||||
properties = obj.properties,
|
||||
proposed = obj.proposed,
|
||||
since = obj.since,
|
||||
extends = {},
|
||||
mixins = {},
|
||||
}
|
||||
end
|
||||
obj = self.known_literals[obj]
|
||||
end
|
||||
print(("Registering %s"):format(obj.name))
|
||||
if not self.known_objs[obj.name] then
|
||||
self.known_objs[obj.name] = obj
|
||||
end
|
||||
return obj
|
||||
end
|
||||
|
||||
---@param name string
|
||||
---@return string
|
||||
function Generator:convert_method_name(name)
|
||||
local new_name = name:gsub("/", "_"):gsub("%$", "_")
|
||||
return new_name
|
||||
end
|
||||
|
||||
---@return string
|
||||
function Generator:type_prefix()
|
||||
return "nio.lsp.types"
|
||||
end
|
||||
|
||||
---@param orig_name string
|
||||
---@return string
|
||||
function Generator:structure_name(orig_name)
|
||||
return self:type_prefix() .. "." .. orig_name
|
||||
end
|
||||
|
||||
---@param items ReferenceType[]
|
||||
---@return Structure
|
||||
function Generator:and_type(items)
|
||||
local names = vim.tbl_map(function(item)
|
||||
return item.name
|
||||
end, items)
|
||||
local sub_structure = {
|
||||
name = table.concat(names, "And"),
|
||||
documentation = "",
|
||||
extends = items,
|
||||
properties = {},
|
||||
mixins = {},
|
||||
proposed = nil,
|
||||
since = nil,
|
||||
}
|
||||
return sub_structure
|
||||
end
|
||||
|
||||
---@param name Name | Name1
|
||||
---@return string
|
||||
function Generator:key_name_type(name)
|
||||
if name == "URI" then
|
||||
return self:type_prefix() .. ".URI"
|
||||
elseif name == "DocumentUri" then
|
||||
return self:type_prefix() .. ".DocumentUri"
|
||||
else
|
||||
return name
|
||||
end
|
||||
end
|
||||
|
||||
---@param type_ Type | MapKeyType)
|
||||
---@return string
|
||||
function Generator:type_name(type_)
|
||||
if type_.kind == "base" then
|
||||
local name = type_.name
|
||||
if name == "integer" or name == "uinteger" then
|
||||
return "integer"
|
||||
elseif name == "decimal" then
|
||||
return "number"
|
||||
elseif name == "string" then
|
||||
return "string"
|
||||
elseif name == "boolean" then
|
||||
return "boolean"
|
||||
elseif name == "null" then
|
||||
return "nil"
|
||||
else
|
||||
return self:key_name_type(name)
|
||||
end
|
||||
elseif type_.kind == "reference" then
|
||||
local name = type_.name
|
||||
return self:structure_name(name)
|
||||
elseif type_.kind == "array" then
|
||||
local element = type_.element
|
||||
return self:type_name(element) .. "[]"
|
||||
elseif type_.kind == "map" then
|
||||
local key, value = type_.key, type_.value
|
||||
if key.kind == "reference" then
|
||||
return ("table<%s, %s>"):format(self:type_name(key), self:type_name(value))
|
||||
else
|
||||
local name = key.name
|
||||
return ("table<%s, %s>"):format(self:key_name_type(name), self:type_name(value))
|
||||
end
|
||||
elseif type_.kind == "and" then
|
||||
local items = type_.items
|
||||
local refs = {}
|
||||
for _, item in ipairs(items) do
|
||||
if item.kind == "reference" then
|
||||
refs[#refs + 1] = item
|
||||
end
|
||||
end
|
||||
if #items > #refs then
|
||||
print(("Discarding non-reference/literal types from AndType"):format())
|
||||
end
|
||||
local struc = self:and_type(refs)
|
||||
self:register(struc)
|
||||
return self:structure_name(struc.name)
|
||||
elseif type_.kind == "or" then
|
||||
local items = type_.items
|
||||
local names = vim.tbl_map(function(item)
|
||||
return self:type_name(item)
|
||||
end, items)
|
||||
|
||||
return table.concat(names, "|")
|
||||
elseif type_.kind == "tuple" then
|
||||
local items = type_.items
|
||||
local names = vim.tbl_map(function(item)
|
||||
return self:type_name(item)
|
||||
end, items)
|
||||
|
||||
return table.concat(names, ",")
|
||||
elseif type_.kind == "literal" then
|
||||
local value = type_.value
|
||||
local struc = self:register(value)
|
||||
return self:structure_name(struc.name)
|
||||
elseif type_.kind == "stringLiteral" then
|
||||
local value = type_.value
|
||||
return ("'%s'"):format(value)
|
||||
end
|
||||
error("Unknown type " .. type_.kind)
|
||||
end
|
||||
|
||||
---@param doc string
|
||||
---@param multiline boolean
|
||||
---@return string[]
|
||||
function Generator:prepare_doc(doc, multiline)
|
||||
local lines = vim.split(doc, "\n", { trimempty = false, plain = true })
|
||||
if multiline then
|
||||
return vim.tbl_map(function(line)
|
||||
return #line and ("--- %s"):format(line) or "---"
|
||||
end, lines)
|
||||
end
|
||||
return { table.concat(lines, " ") }
|
||||
end
|
||||
|
||||
---@param structure Structure
|
||||
---@return string[]
|
||||
function Generator:structure(structure)
|
||||
local lines = { "" }
|
||||
if structure.documentation then
|
||||
vim.list_extend(lines, self:prepare_doc(structure.documentation, true))
|
||||
end
|
||||
|
||||
lines[#lines + 1] = ("---@class %s"):format(self:structure_name(structure.name))
|
||||
if structure.extends or structure.mixins then
|
||||
local extends = vim.list_extend(vim.deepcopy(structure.extends or {}), structure.mixins or {})
|
||||
local names = vim.tbl_map(function(type_)
|
||||
return self:type_name(type_)
|
||||
end, extends)
|
||||
if #names > 0 then
|
||||
lines[#lines] = lines[#lines] .. " : " .. table.concat(names, ",")
|
||||
end
|
||||
end
|
||||
for _, prop in ipairs(structure.properties) do
|
||||
local line = ("---@field %s%s %s"):format(
|
||||
prop.name,
|
||||
prop.optional and "?" or "",
|
||||
self:type_name(prop.type)
|
||||
)
|
||||
if prop.documentation then
|
||||
line = line .. " " .. self:prepare_doc(prop.documentation, false)[1]
|
||||
end
|
||||
lines[#lines + 1] = line
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
---@param type_alias TypeAlias
|
||||
---@return string[]
|
||||
function Generator:type_alias(type_alias)
|
||||
self:register(type_alias)
|
||||
return {
|
||||
("---@alias %s.%s %s"):format(
|
||||
self:type_prefix(),
|
||||
type_alias.name,
|
||||
self:type_name(type_alias.type)
|
||||
),
|
||||
}
|
||||
end
|
||||
|
||||
---@param request Request
|
||||
---@return string[]
|
||||
function Generator:request(request)
|
||||
local lines = {}
|
||||
if request.documentation then
|
||||
vim.list_extend(lines, self:prepare_doc(request.documentation, true))
|
||||
end
|
||||
|
||||
lines[#lines + 1] = "---@async"
|
||||
if request.params then
|
||||
lines[#lines + 1] = ("---@param args %s Arguments to the request"):format(self:type_name(request.params))
|
||||
end
|
||||
lines[#lines + 1] = "---@param bufnr integer? Buffer number (0 for current buffer)"
|
||||
lines[#lines + 1] = "---@param opts? nio.lsp.RequestOpts Options for the request handling"
|
||||
lines[#lines + 1] = ("---@return %s.ResponseError|nil error The error object in case a request fails."):format(self:type_prefix())
|
||||
if request.result then
|
||||
lines[#lines + 1] = (("---@return %s"):format(self:type_name(request.result)))
|
||||
if not vim.endswith(lines[#lines], "|nil") then
|
||||
lines[#lines] = lines[#lines] .. "|nil"
|
||||
end
|
||||
lines[#lines] = lines[#lines] .. " result The result of the request"
|
||||
end
|
||||
|
||||
lines[#lines + 1] = (
|
||||
("function LSPRequestClient.%s(%sbufnr, opts) end"):format(
|
||||
self:convert_method_name(request.method),
|
||||
request.params and "args, " or ""
|
||||
)
|
||||
)
|
||||
lines[#lines + 1] = ""
|
||||
return lines
|
||||
end
|
||||
|
||||
---@param notification Notification
|
||||
---@return string[]
|
||||
function Generator:notification(notification)
|
||||
local lines = {}
|
||||
if notification.documentation then
|
||||
vim.list_extend(lines, self:prepare_doc(notification.documentation, true))
|
||||
end
|
||||
|
||||
lines[#lines + 1] = "---@async"
|
||||
if notification.params then
|
||||
lines[#lines + 1] = (("---@param args %s"):format(self:type_name(notification.params)))
|
||||
lines[#lines + 1] = (
|
||||
("function LSPNotifyClient.%s(%s) end"):format(
|
||||
self:convert_method_name(notification.method),
|
||||
notification.params and "args" or ""
|
||||
)
|
||||
)
|
||||
lines[#lines + 1] = ""
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
---@param enum Enumeration
|
||||
---@return string[]
|
||||
function Generator:enumeration(enum)
|
||||
local lines = {}
|
||||
if enum.documentation then
|
||||
vim.list_extend(lines, self:prepare_doc(enum.documentation, true))
|
||||
end
|
||||
lines[#lines + 1] = (
|
||||
("---@alias %s.%s %s"):format(
|
||||
self:type_prefix(),
|
||||
enum.name,
|
||||
table.concat(
|
||||
vim.tbl_map(function(val)
|
||||
return vim.json.encode(val.value)
|
||||
end, enum.values),
|
||||
"|"
|
||||
)
|
||||
)
|
||||
)
|
||||
lines[#lines + 1] = ""
|
||||
return lines
|
||||
end
|
||||
|
||||
function Generator:generate()
|
||||
local lines = {
|
||||
("---Generated on %s"):format(os.date("!%Y-%m-%d-%H:%M:%S GMT")),
|
||||
"",
|
||||
"---@class nio.lsp.RequestClient",
|
||||
"local LSPRequestClient = {}",
|
||||
"---@class nio.lsp.RequestOpts",
|
||||
"---@field timeout integer Timeout of request in milliseconds",
|
||||
"---@class nio.lsp.types.ResponseError",
|
||||
"---@field code number A number indicating the error type that occurred.",
|
||||
"---@field message string A string providing a short description of the error.",
|
||||
"---@field data any A Primitive or Structured value that contains additional information about the error. Can be omitted.",
|
||||
"",
|
||||
}
|
||||
local strucs = {}
|
||||
vim.list_extend(strucs, self.model.structures)
|
||||
vim.list_extend(strucs, self.model.typeAliases)
|
||||
vim.list_extend(strucs, self.model.enumerations)
|
||||
for _, obj in ipairs(strucs) do
|
||||
self:register(obj)
|
||||
end
|
||||
print("Generating requests")
|
||||
for _, request in ipairs(self.model.requests) do
|
||||
if request.messageDirection == "clientToServer" or request.messageDirection == "both" then
|
||||
vim.list_extend(lines, self:request(request))
|
||||
end
|
||||
end
|
||||
vim.list_extend(lines, {
|
||||
"---@class nio.lsp.NotifyClient",
|
||||
"local LSPNotifyClient = {}",
|
||||
"",
|
||||
})
|
||||
print("Generating notifications")
|
||||
for _, notification in ipairs(self.model.notifications) do
|
||||
if notification.messageDirection == "clientToServer" or notification.messageDirection == "both"
|
||||
then
|
||||
vim.list_extend(lines, self:notification(notification))
|
||||
end
|
||||
end
|
||||
vim.list_extend(lines, {
|
||||
("---@alias %s string"):format(self:key_name_type("URI")),
|
||||
("---@alias %s string"):format(self:key_name_type("DocumentUri")),
|
||||
})
|
||||
|
||||
local length = function()
|
||||
return #vim.tbl_keys(self.known_objs)
|
||||
end
|
||||
local last_length = 0
|
||||
|
||||
print("Discovering types")
|
||||
while length() > last_length do
|
||||
for _, obj in pairs(self.known_objs) do
|
||||
if obj.properties then
|
||||
self:structure(obj)
|
||||
elseif obj.values then
|
||||
self:enumeration(obj)
|
||||
else
|
||||
self:type_alias(obj)
|
||||
end
|
||||
end
|
||||
last_length = length()
|
||||
end
|
||||
|
||||
print("Generating structures")
|
||||
for _, obj in pairs(self.known_objs) do
|
||||
if obj.properties then
|
||||
vim.list_extend(lines, self:structure(obj))
|
||||
elseif obj.values then
|
||||
vim.list_extend(lines, self:enumeration(obj))
|
||||
else
|
||||
vim.list_extend(lines, self:type_alias(obj))
|
||||
end
|
||||
end
|
||||
print(("Generated %d lines\n"):format(#lines))
|
||||
return lines
|
||||
end
|
||||
|
||||
local file = assert(io.open("lsp.json"))
|
||||
local model = vim.json.decode(file:read("*a"))
|
||||
file:close()
|
||||
local lines = Generator.new(model):generate()
|
||||
|
||||
local out = assert(io.open("lua/nio/lsp-types.lua", "w"))
|
||||
out:write(table.concat(lines, "\n"))
|
||||
out:close()
|
||||
vim.cmd("exit")
|
3
scripts/style
Executable file
3
scripts/style
Executable file
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
|
||||
stylua lua tests
|
20
scripts/test
Executable file
20
scripts/test
Executable file
|
@ -0,0 +1,20 @@
|
|||
#!/bin/bash
|
||||
tempfile=".test_output.tmp"
|
||||
|
||||
if [[ -n $1 ]]; then
|
||||
nvim --headless --noplugin -u tests/init.vim -c "PlenaryBustedFile $1" | tee "${tempfile}"
|
||||
else
|
||||
nvim --headless --noplugin -u tests/init.vim -c "PlenaryBustedDirectory tests/ {minimal_init = 'tests/init.vim'}" | tee "${tempfile}"
|
||||
fi
|
||||
|
||||
# Plenary doesn't emit exit code 1 when tests have errors during setup
|
||||
errors=$(sed 's/\x1b\[[0-9;]*m//g' "${tempfile}" | awk '/(Errors|Failed) :/ {print $3}' | grep -v '0')
|
||||
|
||||
rm "${tempfile}"
|
||||
|
||||
if [[ -n $errors ]]; then
|
||||
echo "Tests failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
5
stylua.toml
Normal file
5
stylua.toml
Normal file
|
@ -0,0 +1,5 @@
|
|||
column_width = 100
|
||||
line_endings = "Unix"
|
||||
indent_type = "Spaces"
|
||||
indent_width = 2
|
||||
quote_style = "AutoPreferDouble"
|
16
test.lua
Normal file
16
test.lua
Normal file
|
@ -0,0 +1,16 @@
|
|||
local nio = require("nio")
|
||||
|
||||
nio.run(function()
|
||||
|
||||
local client = nio.lsp.get_clients({ name = "lua_ls" })[1]
|
||||
|
||||
local err, response = client.request.textDocument_semanticTokens_full({
|
||||
textDocument = { uri = vim.uri_from_bufnr(0) },
|
||||
})
|
||||
|
||||
assert(not err, err)
|
||||
|
||||
for _, i in pairs(response and response.data or {}) do
|
||||
print(i)
|
||||
end
|
||||
end)
|
145
tests/control_spec.lua
Normal file
145
tests/control_spec.lua
Normal file
|
@ -0,0 +1,145 @@
|
|||
local nio = require("nio")
|
||||
local a = nio.tests
|
||||
|
||||
describe("event", function()
|
||||
a.it("notifies listeners", function()
|
||||
local event = nio.control.event()
|
||||
local notified = 0
|
||||
for _ = 1, 10 do
|
||||
nio.run(function()
|
||||
event.wait()
|
||||
notified = notified + 1
|
||||
end)
|
||||
end
|
||||
|
||||
event.set()
|
||||
nio.sleep(10)
|
||||
assert.equals(10, notified)
|
||||
end)
|
||||
|
||||
a.it("notifies listeners when already set", function()
|
||||
local event = nio.control.event()
|
||||
local notified = 0
|
||||
event.set()
|
||||
for _ = 1, 10 do
|
||||
nio.run(function()
|
||||
event.wait()
|
||||
notified = notified + 1
|
||||
end)
|
||||
end
|
||||
|
||||
nio.sleep(10)
|
||||
assert.equals(10, notified)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("future", function()
|
||||
a.it("provides listeners result", function()
|
||||
local future = nio.control.future()
|
||||
local notified = 0
|
||||
for _ = 1, 10 do
|
||||
nio.run(function()
|
||||
local val = future.wait()
|
||||
notified = notified + val
|
||||
end)
|
||||
end
|
||||
|
||||
future.set(1)
|
||||
nio.sleep(10)
|
||||
assert.equals(10, notified)
|
||||
end)
|
||||
|
||||
a.it("notifies listeners when already set", function()
|
||||
local future = nio.control.future()
|
||||
local notified = 0
|
||||
future.set(1)
|
||||
for _ = 1, 10 do
|
||||
nio.run(function()
|
||||
notified = notified + future.wait()
|
||||
end)
|
||||
end
|
||||
|
||||
nio.sleep(10)
|
||||
assert.equals(10, notified)
|
||||
end)
|
||||
|
||||
a.it("raises error for listeners", function()
|
||||
local future = nio.control.future()
|
||||
local notified = 0
|
||||
future.set_error("test")
|
||||
local success, err = pcall(future.wait)
|
||||
|
||||
nio.sleep(10)
|
||||
assert.False(success)
|
||||
assert.True(vim.endswith(err, "test"))
|
||||
end)
|
||||
end)
|
||||
describe("queue", function()
|
||||
a.it("adds and removes items", function()
|
||||
local queue = nio.control.queue()
|
||||
queue.put(1)
|
||||
queue.put(2)
|
||||
|
||||
assert.same(queue.size(), 2)
|
||||
assert.same(1, queue.get())
|
||||
assert.same(2, queue.get())
|
||||
assert.same(queue.size(), 0)
|
||||
end)
|
||||
|
||||
a.it("get blocks while empty", function()
|
||||
local queue = nio.control.queue()
|
||||
nio.run(function()
|
||||
nio.sleep(10)
|
||||
queue.put(1)
|
||||
end)
|
||||
assert.same(1, queue.get())
|
||||
end)
|
||||
|
||||
a.it("put blocks while full", function()
|
||||
local queue = nio.control.queue(1)
|
||||
nio.run(function()
|
||||
nio.sleep(10)
|
||||
queue.get()
|
||||
end)
|
||||
queue.put(1)
|
||||
queue.put(2)
|
||||
assert.same(2, queue.get())
|
||||
end)
|
||||
|
||||
it("get_nowait errors when empty", function()
|
||||
local queue = nio.control.queue()
|
||||
assert.error(queue.get_nowait)
|
||||
end)
|
||||
|
||||
it("put_nowait errors while full", function()
|
||||
local queue = nio.control.queue(1)
|
||||
queue.put_nowait(1)
|
||||
assert.error(function()
|
||||
queue.put_nowait(2)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("semaphore", function()
|
||||
a.it("only allows permitted number of concurrent accesses", function()
|
||||
local concurrent = 0
|
||||
local max_concurrent = 0
|
||||
local allowed = 3
|
||||
local semaphore = nio.control.semaphore(allowed)
|
||||
local worker = function()
|
||||
semaphore.with(function()
|
||||
concurrent = concurrent + 1
|
||||
max_concurrent = math.max(max_concurrent, concurrent)
|
||||
nio.sleep(10)
|
||||
concurrent = concurrent - 1
|
||||
end)
|
||||
end
|
||||
local workers = {}
|
||||
for _ = 1, 10 do
|
||||
table.insert(workers, worker)
|
||||
end
|
||||
nio.gather(workers)
|
||||
|
||||
assert.same(max_concurrent, allowed)
|
||||
end)
|
||||
end)
|
1
tests/init.vim
Normal file
1
tests/init.vim
Normal file
|
@ -0,0 +1 @@
|
|||
source tests/minimal_init.lua
|
118
tests/init_spec.lua
Normal file
118
tests/init_spec.lua
Normal file
|
@ -0,0 +1,118 @@
|
|||
local nio = require("nio")
|
||||
local a = nio.tests
|
||||
|
||||
describe("async helpers", function()
|
||||
a.it("sleep", function()
|
||||
local start = vim.loop.now()
|
||||
nio.sleep(10)
|
||||
local end_ = vim.loop.now()
|
||||
assert.True(end_ - start >= 10)
|
||||
end)
|
||||
|
||||
a.it("wrap returns values provided to callback", function()
|
||||
local result
|
||||
local wrapped = nio.wrap(function(_, _, cb)
|
||||
cb(1, 2)
|
||||
end, 3)
|
||||
nio.run(wrapped, function(_, ...)
|
||||
result = { ... }
|
||||
end)
|
||||
|
||||
assert.same({ 1, 2 }, result)
|
||||
end)
|
||||
|
||||
a.it("gather returns results", function()
|
||||
local worker = function(i)
|
||||
return function()
|
||||
nio.sleep(100 - (i * 10))
|
||||
return i
|
||||
end
|
||||
end
|
||||
|
||||
local workers = {}
|
||||
for i = 1, 10 do
|
||||
table.insert(workers, worker(i))
|
||||
end
|
||||
|
||||
local results = nio.gather(workers)
|
||||
assert.same({ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, results)
|
||||
end)
|
||||
|
||||
a.it("gather raises errors", function()
|
||||
local worker = function(i)
|
||||
return function()
|
||||
if i == 4 then
|
||||
error("error")
|
||||
end
|
||||
nio.sleep(100 - (i * 10))
|
||||
return i
|
||||
end
|
||||
end
|
||||
|
||||
local workers = {}
|
||||
for i = 1, 10 do
|
||||
table.insert(workers, worker(i))
|
||||
end
|
||||
|
||||
assert.error(function()
|
||||
nio.gather(workers)
|
||||
end)
|
||||
end)
|
||||
|
||||
a.it("first returns first result", function()
|
||||
local worker = function(i)
|
||||
return function()
|
||||
nio.sleep(100 - (i * 10))
|
||||
return i
|
||||
end
|
||||
end
|
||||
|
||||
local workers = {}
|
||||
for i = 1, 10 do
|
||||
table.insert(workers, worker(i))
|
||||
end
|
||||
|
||||
local result = nio.first(workers)
|
||||
assert.same(10, result)
|
||||
end)
|
||||
|
||||
a.it("first cancels pending tasks", function()
|
||||
local worker = function(i)
|
||||
return function()
|
||||
nio.sleep(100 - (i * 10))
|
||||
if i ~= 10 then
|
||||
error("error")
|
||||
end
|
||||
return i
|
||||
end
|
||||
end
|
||||
|
||||
local workers = {}
|
||||
for i = 1, 10 do
|
||||
table.insert(workers, worker(i))
|
||||
end
|
||||
|
||||
nio.first(workers)
|
||||
end)
|
||||
|
||||
a.it("first raises errors", function()
|
||||
local worker = function(i)
|
||||
return function()
|
||||
if i == 4 then
|
||||
error("error")
|
||||
end
|
||||
nio.sleep(100 - (i * 10))
|
||||
return i
|
||||
end
|
||||
end
|
||||
|
||||
local workers = {}
|
||||
for i = 1, 10 do
|
||||
table.insert(workers, worker(i))
|
||||
end
|
||||
|
||||
assert.error(function()
|
||||
nio.first(workers)
|
||||
end)
|
||||
end)
|
||||
end)
|
96
tests/lsp_spec.lua
Normal file
96
tests/lsp_spec.lua
Normal file
|
@ -0,0 +1,96 @@
|
|||
local nio = require("nio")
|
||||
local a = nio.tests
|
||||
|
||||
describe("lsp client", function()
|
||||
a.it("sends request and returns result", function()
|
||||
local expected_result = { "test" }
|
||||
local expected_params = { a = "b" }
|
||||
vim.lsp.get_client_by_id = function(id)
|
||||
return {
|
||||
request = function(method, params, callback, bufnr)
|
||||
assert.equals("textDocument/diagnostic", method)
|
||||
assert.equals(0, bufnr)
|
||||
assert.same(params, params)
|
||||
callback(nil, expected_result)
|
||||
return true, 1
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local client = nio.lsp.client(1)
|
||||
|
||||
local err, result =
|
||||
client.request.textDocument_diagnostic(expected_params, 0, { timeout = 1000 })
|
||||
assert.same(expected_result, result)
|
||||
end)
|
||||
|
||||
a.it("returns error for request", function()
|
||||
local params = { a = "b" }
|
||||
vim.lsp.get_client_by_id = function(id)
|
||||
return {
|
||||
request = function(method, params, callback, bufnr)
|
||||
callback({ message = "error" }, nil)
|
||||
return true, 1
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local client = nio.lsp.client(1)
|
||||
|
||||
local err, result = client.request.textDocument_diagnostic(0, params)
|
||||
assert.same(err.message, "error")
|
||||
assert.Nil(result)
|
||||
end)
|
||||
|
||||
a.it("raises error on timeout", function()
|
||||
vim.lsp.get_client_by_id = function(id)
|
||||
return {
|
||||
request = function(method, params, callback, bufnr)
|
||||
return true, 1
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local client = nio.lsp.client(1)
|
||||
|
||||
local err, result = client.request.textDocument_diagnostic({}, 0, { timeout = 10 })
|
||||
assert.same(err.message, "Request timed out")
|
||||
assert.Nil(result)
|
||||
end)
|
||||
|
||||
a.it("cancels request on timeout", function()
|
||||
local cancel_received = false
|
||||
vim.lsp.get_client_by_id = function(id)
|
||||
return {
|
||||
request = function(method, params, callback, bufnr)
|
||||
if method == "$/cancelRequest" then
|
||||
cancel_received = true
|
||||
end
|
||||
return true, 1
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local client = nio.lsp.client(1)
|
||||
|
||||
client.request.textDocument_diagnostic({}, 0, { timeout = 10 })
|
||||
assert.True(cancel_received)
|
||||
end)
|
||||
|
||||
a.it("raises errors on client shutdown", function()
|
||||
vim.lsp.get_client_by_id = function(id)
|
||||
return {
|
||||
id = id,
|
||||
request = function(method, params, callback, bufnr)
|
||||
return false
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local client = nio.lsp.client(1)
|
||||
|
||||
local success, err = pcall(client.request.textDocument_diagnostic, {}, 0, { timeout = 10 })
|
||||
assert.False(success)
|
||||
assert.Not.Nil(string.find(err, "Client 1 has shut down"))
|
||||
end)
|
||||
end)
|
9
tests/minimal_init.lua
Normal file
9
tests/minimal_init.lua
Normal file
|
@ -0,0 +1,9 @@
|
|||
local lazypath = vim.fn.stdpath("data") .. "/lazy"
|
||||
vim.notify = print
|
||||
vim.opt.rtp:append(".")
|
||||
vim.opt.rtp:append(lazypath .. "/plenary.nvim")
|
||||
vim.cmd("runtime! plugin/plenary.vim")
|
||||
vim.opt.swapfile = false
|
||||
A = function(...)
|
||||
print(vim.inspect(...))
|
||||
end
|
120
tests/tasks_spec.lua
Normal file
120
tests/tasks_spec.lua
Normal file
|
@ -0,0 +1,120 @@
|
|||
local tasks = require("nio.tasks")
|
||||
local nio = require("nio")
|
||||
local a = nio.tests
|
||||
|
||||
describe("task", function()
|
||||
a.it("provides result in callback", function()
|
||||
local result
|
||||
tasks.run(function()
|
||||
nio.sleep(5)
|
||||
return "test"
|
||||
end, function(_, result_)
|
||||
result = result_
|
||||
end)
|
||||
nio.sleep(10)
|
||||
assert.equals("test", result)
|
||||
end)
|
||||
|
||||
a.it("cancels", function()
|
||||
local err
|
||||
local task = tasks.run(function()
|
||||
nio.sleep(10)
|
||||
return "test"
|
||||
end, function(_, err_)
|
||||
err = err_
|
||||
end)
|
||||
task.cancel()
|
||||
nio.sleep(10)
|
||||
assert.True(vim.endswith(vim.split(err, "\n")[1], "Task was cancelled"))
|
||||
end)
|
||||
|
||||
a.it("cancels children", function()
|
||||
local should_be_nil
|
||||
local task = tasks.run(function()
|
||||
tasks.run(function()
|
||||
nio.sleep(10)
|
||||
should_be_nil = "not nil"
|
||||
end)
|
||||
nio.sleep(10)
|
||||
end)
|
||||
task.cancel()
|
||||
nio.sleep(20)
|
||||
assert.Nil(should_be_nil)
|
||||
end)
|
||||
|
||||
a.it("assigns parent task", function()
|
||||
local current = tasks.current_task()
|
||||
local task = tasks.run(function()
|
||||
return "test"
|
||||
end)
|
||||
assert.Not.Nil(task.parent)
|
||||
assert.equal(current, task.parent)
|
||||
end)
|
||||
|
||||
it("assigns no parent task", function()
|
||||
local task = tasks.run(function()
|
||||
return "test"
|
||||
end)
|
||||
assert.Nil(task.parent)
|
||||
end)
|
||||
|
||||
a.it("returns error in function", function()
|
||||
local success, err
|
||||
tasks.run(function()
|
||||
error("test")
|
||||
end, function(success_, err_)
|
||||
success, err = success_, err_
|
||||
end)
|
||||
nio.sleep(10)
|
||||
assert.False(success)
|
||||
assert.True(vim.endswith(vim.split(err, "\n")[2], "test"))
|
||||
end)
|
||||
|
||||
a.it("returns error when wrapped function errors", function()
|
||||
local success, err
|
||||
local bad_wrapped = tasks.wrap(function()
|
||||
error("test")
|
||||
end, 1)
|
||||
tasks.run(bad_wrapped, function(success_, err_)
|
||||
success, err = success_, err_
|
||||
end)
|
||||
nio.sleep(10)
|
||||
assert.False(success)
|
||||
assert.True(vim.endswith(vim.split(err, "\n")[2], "test"))
|
||||
end)
|
||||
|
||||
a.it("pcall returns result", function()
|
||||
local success, x, y = pcall(function()
|
||||
return 1, 2
|
||||
end)
|
||||
assert.True(success)
|
||||
assert.equals(1, x)
|
||||
assert.equals(2, y)
|
||||
end)
|
||||
|
||||
a.it("pcall returns error", function()
|
||||
local success, err = pcall(function()
|
||||
error("test")
|
||||
end)
|
||||
assert.False(success)
|
||||
assert.True(vim.endswith(vim.split(err, "\n")[1], "test"))
|
||||
end)
|
||||
|
||||
a.it("pcall returns error when wrapped function errors", function()
|
||||
local success, err = pcall(tasks.wrap(function(...)
|
||||
error("test")
|
||||
end, 1))
|
||||
|
||||
nio.sleep(10)
|
||||
assert.False(success)
|
||||
assert.True(vim.endswith(vim.split(err, "\n")[1], "test"))
|
||||
end)
|
||||
|
||||
a.it("current task", function()
|
||||
local current
|
||||
local task = tasks.run(function()
|
||||
current = tasks.current_task()
|
||||
end)
|
||||
assert.equal(task, current)
|
||||
end)
|
||||
end)
|
31
tests/uv_spec.lua
Normal file
31
tests/uv_spec.lua
Normal file
|
@ -0,0 +1,31 @@
|
|||
local nio = require("nio")
|
||||
local a = nio.tests
|
||||
|
||||
describe("file operations", function()
|
||||
local path = vim.fn.tempname()
|
||||
a.after_each(function()
|
||||
os.remove(path)
|
||||
end)
|
||||
a.it("reads a file", function()
|
||||
local f = assert(io.open(path, "w"))
|
||||
f:write("test read")
|
||||
f:close()
|
||||
|
||||
local _, file = nio.uv.fs_open(path, "r", 438)
|
||||
local _, data = nio.uv.fs_read(file, 1024, -1)
|
||||
nio.uv.fs_close(file)
|
||||
assert.equals("test read", data)
|
||||
end)
|
||||
|
||||
a.it("writes a file", function()
|
||||
local _, file = nio.uv.fs_open(path, "w", 438)
|
||||
nio.uv.fs_write(file, "test write")
|
||||
nio.uv.fs_close(file)
|
||||
|
||||
local file = assert(io.open(path, "r"))
|
||||
local data = file:read()
|
||||
file:close()
|
||||
|
||||
assert.equals("test write", data)
|
||||
end)
|
||||
end)
|
Loading…
Reference in a new issue