1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-04-14 15:19:12 +08:00

feat(layer): add telescope layer

This commit is contained in:
Wang Shidong 2022-05-16 22:20:10 +08:00 committed by GitHub
parent 48c57041f9
commit 7237a74889
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
265 changed files with 47812 additions and 10 deletions

View File

@ -0,0 +1,315 @@
"=============================================================================
" telescope.vim --- telescope support for spacevim
" Copyright (c) 2016-2019 Wang Shidong & Contributors
" Author: Wang Shidong < wsdjeg@outlook.com >
" URL: https://spacevim.org
" License: GPLv3
"=============================================================================
""
" @section telescope, layers-telescope
" @parentsection layers
" This layer provides fuzzy finder feature which is based on |telescope|, and this
" This layer is not loaded by default. To use this layer:
" >
" [[layers]]
" name = 'telescope'
" <
" @subsection Key bindings
"
" The following key bindings will be enabled when this layer is loaded:
" >
" Key bindings Description
" SPC p f / Ctrl-p search files in current directory
" <Leader> f SPC Fuzzy find menu:CustomKeyMaps
" <Leader> f e Fuzzy find register
" <Leader> f h Fuzzy find history/yank
" <Leader> f j Fuzzy find jump, change
" <Leader> f l Fuzzy find location list
" <Leader> f m Fuzzy find output messages
" <Leader> f o Fuzzy find functions
" <Leader> f t Fuzzy find tags
" <Leader> f q Fuzzy find quick fix
" <Leader> f r Resumes Unite window
" <
function! SpaceVim#layers#telescope#plugins() abort
let plugins = []
call add(plugins, [g:_spacevim_root_dir . 'bundle/telescope.nvim', {'merged' : 0, 'loadconf' : 1}])
call add(plugins, [g:_spacevim_root_dir . 'bundle/plenary.nvim', {'merged' : 0}])
call add(plugins, [g:_spacevim_root_dir . 'bundle/telescope-menu', {'merged' : 0}])
call add(plugins, [g:_spacevim_root_dir . 'bundle/telescope-ctags-outline.nvim', {'merged' : 0}])
return plugins
endfunction
let s:filename = expand('<sfile>:~')
let s:lnum = expand('<slnum>') + 2
function! SpaceVim#layers#telescope#config() abort
let lnum = expand('<slnum>') + s:lnum - 1
call SpaceVim#mapping#space#def('nnoremap', ['?'], 'call call('
\ . string(s:_function('s:get_menu')) . ', ["CustomKeyMaps", "[SPC]"])',
\ ['show-mappings',
\ [
\ 'SPC ? is to show mappings',
\ '',
\ 'Definition: ' . s:filename . ':' . lnum,
\ ]
\ ],
\ 1)
let lnum = expand('<slnum>') + s:lnum - 1
call SpaceVim#mapping#space#def('nnoremap', ['h', '[SPC]'], 'call call('
\ . string(s:_function('s:get_help')) . ', ["SpaceVim"])',
\ ['find-SpaceVim-help',
\ [
\ 'SPC h SPC is to find SpaceVim help',
\ '',
\ 'Definition: ' . s:filename . ':' . lnum,
\ ]
\ ],
\ 1)
" @fixme SPC h SPC make vim flick
exe printf('nmap %sh%s [SPC]h[SPC]', g:spacevim_default_custom_leader, g:spacevim_default_custom_leader)
let lnum = expand('<slnum>') + s:lnum - 1
call SpaceVim#mapping#space#def('nnoremap', ['b', 'b'], 'Telescope buffers',
\ ['list-buffer',
\ [
\ 'SPC b b is to open buffer list',
\ '',
\ 'Definition: ' . s:filename . ':' . lnum,
\ ]
\ ],
\ 1)
let lnum = expand('<slnum>') + s:lnum - 1
call SpaceVim#mapping#space#def('nnoremap', ['f', 'r'], 'Telescope oldfiles',
\ ['open-recent-file',
\ [
\ 'SPC f r is to open recent file list',
\ '',
\ 'Definition: ' . s:filename . ':' . lnum,
\ ]
\ ],
\ 1)
let lnum = expand('<slnum>') + s:lnum - 1
call SpaceVim#mapping#space#def('nnoremap', ['j', 'i'], 'Telescope ctags_outline outline',
\ ['jump-to-definition-in-buffer',
\ [
\ 'SPC j i is to jump to a definition in buffer',
\ '',
\ 'Definition: ' . s:filename . ':' . lnum,
\ ]
\ ],
\ 1)
let lnum = expand('<slnum>') + s:lnum - 1
call SpaceVim#mapping#space#def('nnoremap', ['T', 's'], 'Telescope colorscheme',
\ ['fuzzy-find-colorschemes',
\ [
\ 'SPC T s is to fuzzy find colorschemes',
\ '',
\ 'Definition: ' . s:filename . ':' . lnum,
\ ]
\ ],
\ 1)
let lnum = expand('<slnum>') + s:lnum - 1
call SpaceVim#mapping#space#def('nnoremap', ['f', 'f'],
\ "exe 'CtrlP ' . fnamemodify(bufname('%'), ':h')",
\ ['find-files-in-buffer-directory',
\ [
\ '[SPC f f] is to find files in the directory of the current buffer',
\ '',
\ 'Definition: ' . s:filename . ':' . lnum,
\ ]
\ ]
\ , 1)
let lnum = expand('<slnum>') + s:lnum - 1
call SpaceVim#mapping#space#def('nnoremap', ['p', 'f'],
\ 'Telescope find_files',
\ ['find-files-in-project',
\ [
\ '[SPC p f] is to find files in the root of the current project',
\ '',
\ 'Definition: ' . s:filename . ':' . lnum,
\ ]
\ ]
\ , 1)
nnoremap <silent> <C-p> :<C-u>Telescope find_files<cr>
let lnum = expand('<slnum>') + s:lnum - 1
call SpaceVim#mapping#space#def('nnoremap', ['h', 'i'], 'call call('
\ . string(s:_function('s:get_help_with_cursor_symbol')) . ', [])',
\ ['get-help-for-cursor-symbol',
\ [
\ '[SPC h i] is to get help with the symbol at point',
\ '',
\ 'Definition: ' . s:filename . ':' . lnum,
\ ]
\ ],
\ 1)
let g:_spacevim_mappings.f = {'name' : '+Fuzzy Finder'}
call s:defind_fuzzy_finder()
augroup spacevim_telescope_layer
autocmd!
" https://github.com/nvim-telescope/telescope.nvim/issues/161
autocmd FileType TelescopePrompt call deoplete#custom#buffer_option('auto_complete', v:false)
augroup END
endfunction
function! s:get_help_with_cursor_symbol() abort
call v:lua.require('telescope.builtin').help_tags({ 'default_text' : expand('<cword>')})
endfunction
function! s:get_help(word) abort
call v:lua.require('telescope.builtin').help_tags({ 'default_text' : a:word})
endfunction
function! s:get_menu(menu, input) abort
let save_ctrlp_default_input = get(g:, 'ctrlp_default_input', '')
let g:ctrlp_default_input = a:input
exe 'CtrlPMenu ' . a:menu
let g:ctrlp_default_input = save_ctrlp_default_input
endfunction
let s:file = expand('<sfile>:~')
let s:unite_lnum = expand('<slnum>') + 3
function! s:defind_fuzzy_finder() abort
nnoremap <silent> <Leader>fe
\ :<C-u>Telescope registers<CR>
let lnum = expand('<slnum>') + s:unite_lnum - 4
let g:_spacevim_mappings.f.e = ['Telescope registers',
\ 'fuzzy find registers',
\ [
\ '[Leader f e ] is to fuzzy find registers',
\ '',
\ 'Definition: ' . s:file . ':' . lnum,
\ ]
\ ]
nnoremap <silent> <Leader>fr
\ :<C-u>Telescope resume<CR>
let lnum = expand('<slnum>') + s:unite_lnum - 4
let g:_spacevim_mappings.f.r = ['Telescope resume',
\ 'resume telescope window',
\ [
\ '[Leader f r ] is to resume telescope window',
\ '',
\ 'Definition: ' . s:file . ':' . lnum,
\ ]
\ ]
nnoremap <silent> <Leader>fh
\ :<C-u>CtrlPNeoyank<CR>
let lnum = expand('<slnum>') + s:unite_lnum - 4
let g:_spacevim_mappings.f.h = ['CtrlPNeoyank',
\ 'fuzzy find yank history',
\ [
\ '[Leader f h] is to fuzzy find history and yank content',
\ '',
\ 'Definition: ' . s:file . ':' . lnum,
\ ]
\ ]
nnoremap <silent> <Leader>fj
\ :<C-u>Telescope jumplist<CR>
let lnum = expand('<slnum>') + s:unite_lnum - 4
let g:_spacevim_mappings.f.j = ['Telescope jumplist',
\ 'fuzzy find jump list',
\ [
\ '[Leader f j] is to fuzzy find jump list',
\ '',
\ 'Definition: ' . s:file . ':' . lnum,
\ ]
\ ]
nnoremap <silent> <Leader>fl
\ :<C-u>Telescope loclist<CR>
let lnum = expand('<slnum>') + s:unite_lnum - 4
let g:_spacevim_mappings.f.l = ['Telescope loclist',
\ 'fuzzy find local list',
\ [
\ '[Leader f q] is to fuzzy find local list',
\ '',
\ 'Definition: ' . s:file . ':' . lnum,
\ ]
\ ]
nnoremap <silent> <Leader>fm
\ :<C-u>Telescope messages<CR>
let lnum = expand('<slnum>') + s:unite_lnum - 4
let g:_spacevim_mappings.f.m = ['Telescope messages',
\ 'fuzzy find and yank message history',
\ [
\ '[Leader f m] is to fuzzy find and yank message history',
\ '',
\ 'Definition: ' . s:file . ':' . lnum,
\ ]
\ ]
nnoremap <silent> <Leader>fq
\ :<C-u>Telescope quickfix<CR>
let lnum = expand('<slnum>') + s:unite_lnum - 4
let g:_spacevim_mappings.f.q = ['Telescope quickfix',
\ 'fuzzy find quickfix list',
\ [
\ '[Leader f q] is to fuzzy find quickfix list',
\ '',
\ 'Definition: ' . s:file . ':' . lnum,
\ ]
\ ]
nnoremap <silent> <Leader>fo :<C-u>Telescope ctags_outline outline<CR>
let lnum = expand('<slnum>') + s:unite_lnum - 4
let g:_spacevim_mappings.f.o = ['Telescope ctags_outline outline',
\ 'fuzzy find outline',
\ [
\ '[Leader f o] is to fuzzy find outline',
\ '',
\ 'Definition: ' . s:file . ':' . lnum,
\ ]
\ ]
nnoremap <silent> <Leader>f<Space> :CtrlPMenu CustomKeyMaps<CR>
let lnum = expand('<slnum>') + s:unite_lnum - 4
let g:_spacevim_mappings.f['[SPC]'] = ['CtrlPMenu CustomKeyMaps',
\ 'fuzzy find custom key bindings',
\ [
\ '[Leader f SPC] is to fuzzy find custom key bindings',
\ '',
\ 'Definition: ' . s:file . ':' . lnum,
\ ]
\ ]
nnoremap <silent> <Leader>fp :<C-u>CtrlPMenu AddedPlugins<CR>
let lnum = expand('<slnum>') + s:unite_lnum - 4
let g:_spacevim_mappings.f.p = ['CtrlPMenu AddedPlugins',
\ 'fuzzy find vim packages',
\ [
\ '[Leader f p] is to fuzzy find vim packages installed in SpaceVim',
\ '',
\ 'Definition: ' . s:file . ':' . lnum,
\ ]
\ ]
endfunction
" function() wrapper
if v:version > 703 || v:version == 703 && has('patch1170')
function! s:_function(fstr) abort
return function(a:fstr)
endfunction
else
function! s:_SID() abort
return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze__SID$')
endfunction
let s:_s = '<SNR>' . s:_SID() . '_'
function! s:_function(fstr) abort
return function(substitute(a:fstr, 's:', s:_s, 'g'))
endfunction
endif

View File

@ -25,3 +25,5 @@ In `bundle/` directory, there are two kinds of plugins: forked plugins without c
- [deoplete](https://github.com/Shougo/deoplete.nvim/tree/1c40f648d2b00e70beb4c473b7c0e32b633bd9ae)
- [vim-scala@7657218](https://github.com/derekwyatt/vim-scala/tree/7657218f14837395a4e6759f15289bad6febd1b4)
- [neosnippet.vim@5973e80](https://github.com/Shougo/neosnippet.vim/tree/5973e801e7ad38a01e888cb794d74e076a35ea9b)
- [telescope.nvim@39b12d8](https://github.com/nvim-telescope/telescope.nvim/tree/39b12d84e86f5054e2ed98829b367598ae53ab41)
- [telescope-ctags-outline.nvim](https://github.com/fcying/telescope-ctags-outline.nvim)

View File

@ -0,0 +1 @@
github: tjdevries

View File

@ -0,0 +1,59 @@
name: default
on: [push, pull_request]
jobs:
x64-ubuntu:
name: X64-ubuntu
runs-on: ubuntu-20.04
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 }}-x64-${{ hashFiles('todays-date') }}
- name: Run tests
run: |
curl -OL https://raw.githubusercontent.com/norcalli/bot-ci/master/scripts/github-actions-setup.sh
source github-actions-setup.sh nightly-x64
make test
appimage-ubuntu:
name: Appimage-ubuntu
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- run: date +%F > todays-date
- name: Restore cache for today's nightly.
uses: actions/cache@v2
with:
path: build
key: ${{ runner.os }}-appimage-${{ hashFiles('todays-date') }}
- name: Prepare
run: |
test -d build || {
mkdir -p build
wget https://github.com/neovim/neovim/releases/download/nightly/nvim.appimage
chmod +x nvim.appimage
mv nvim.appimage ./build/nvim
}
- name: Run tests
run: |
export PATH="${PWD}/build/:${PATH}"
make test
stylua:
name: stylua
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: JohnnyMorganz/stylua-action@1.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
# CLI arguments
args: --color always --check .

43
bundle/plenary.nvim/.gitignore vendored Normal file
View File

@ -0,0 +1,43 @@
# Compiled Lua sources
luac.out
# luarocks build files
*.src.rock
*.zip
*.tar.gz
# Object files
*.o
*.os
*.ko
*.obj
*.elf
# Precompiled Headers
*.gch
*.pch
# Libraries
*.lib
*.a
*.la
*.lo
*.def
*.exp
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
build/

View File

@ -0,0 +1,6 @@
column_width = 120
line_endings = "Unix"
indent_type = "Spaces"
indent_width = 2
quote_style = "AutoPreferDouble"
call_parentheses = "None"

View File

@ -0,0 +1,10 @@
build/
data/
lua/luassert
lua/plenary/profile.lua
lua/plenary/profile/
lua/plenary/bit.lua
lua/say.lua
scratch/
scripts/

View File

@ -0,0 +1,7 @@
## Pre-Alpha
- Change the keys in tables returned by functions in the `plenary.window.float` module:
- `buf` -> `bufnr`
- `minor_buf` -> `minor_bufnr`
- `border_buf` -> `border_bufnr`
- `win` -> `win_id`
- `minor_win` -> `minor_win_id`

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 TJ DeVries
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.

View File

@ -0,0 +1,5 @@
test:
nvim --headless --noplugin -u scripts/minimal.vim -c "PlenaryBustedDirectory tests/plenary/ {minimal_init = 'tests/minimal_init.vim'}"
generate_filetypes:
nvim --headless -c 'luafile scripts/update_filetypes_from_github.lua' -c 'qa!'

View File

@ -0,0 +1,89 @@
# Popup tracking
[WIP] An implementation of the Popup API from vim in Neovim. Hope to upstream
when complete
## Goals
Provide an API that is compatible with the vim `popup_*` APIs. After
stablization and any required features are merged into Neovim, we can upstream
this and expose the API in vimL to create better compatibility.
## Notices
- **2021-09-19:** we now follow Vim's convention of the first line/column of the screen being indexed 1, so that 0 can be used for centering.
- **2021-08-19:** we now follow Vim's default to `noautocmd` on popup creation. This can be overriden with `vim_options.noautocmd=false`
## List of Neovim Features Required:
- [ ] Key handlers (used for `popup_filter`)
- [ ] scrollbar for floating windows
- [ ] scrollbar
- [ ] scrollbarhighlight
- [ ] thumbhighlight
Optional:
- [ ] Add forced transparency to a floating window.
- Apparently overrides text?
- This is for the `mask` feature flag
Unlikely (due to technical difficulties):
- [ ] Add `textprop` wrappers?
- textprop
- textpropwin
- textpropid
- [ ] "close"
- But this is mostly because I don't know how to use mouse APIs in nvim. If someone knows. please make an issue in the repo, and maybe we can get it sorted out.
Unlikely (due to not sure if people are using):
- [ ] tabpage
## Progress
Suported Features:
- [x] what
- string
- list of strings
- [x] popup_create-arguments
- [x] border
- [x] borderchars
- [x] col
- [x] cursorline
- [x] highlight
- [x] line
- [x] {max,min}{height,width}
- [?] moved
- [x] "any"
- [ ] "word"
- [ ] "WORD"
- [ ] "expr"
- [ ] (list options)
- [x] padding
- [?] pos
- Somewhat implemented. Doesn't work with borders though.
- [x] posinvert
- [x] time
- [x] title
- [x] wrap
- [x] zindex
## All known unimplemented vim features at the moment
- firstline
- hidden
- ~ pos
- fixed
- filter
- filtermode
- mapping
- callback
- mouse:
- mousemoved
- close
- drag
- resize
- (not implemented in vim yet) flip

View File

@ -0,0 +1,335 @@
# plenary.nvim
All the lua functions I don't want to write twice.
> plenary:
>
> full; complete; entire; absolute; unqualified.
Note that this library is useless outside of Neovim since it requires Neovim functions. It should be usable with any recent version of Neovim though.
At the moment, it is very much in pre-alpha :smile: Expect changes to the way some functions are structured. I'm hoping to finish some document generators to provide better documentation for people to use and consume and then at some point we'll stabilize on a few more stable APIs.
## Installation
```vim
Plug 'nvim-lua/plenary.nvim'
```
## Modules
- `plenary.async`
- `plenary.async_lib`
- `plenary.job`
- `plenary.path`
- `plenary.scandir`
- `plenary.context_manager`
- `plenary.test_harness`
- `plenary.filetype`
- `plenary.strings`
### plenary.async
A Lua module for asynchronous programming using coroutines. This library is built on native lua coroutines and `libuv`. Coroutines make it easy to avoid callback hell and allow for easy cooperative concurrency and cancellation. Apart from allowing users to perform asynchronous io easily, this library also functions as an abstraction for coroutines.
#### Getting started
You can do
```lua
local async = require "plenary.async"
```
All other modules are automatically required and can be accessed by indexing `async`.
You needn't worry about performance as this will require all the submodules lazily.
#### A quick example
Libuv luv provides this example of reading a file.
```lua
local uv = vim.loop
local read_file = function(path, callback)
uv.fs_open(path, "r", 438, function(err, fd)
assert(not err, err)
uv.fs_fstat(fd, function(err, stat)
assert(not err, err)
uv.fs_read(fd, stat.size, 0, function(err, data)
assert(not err, err)
uv.fs_close(fd, function(err)
assert(not err, err)
callback(data)
end)
end)
end)
end)
end
```
We can write it using the library like this:
```lua
local a = require "plenary.async"
local read_file = function(path)
local err, fd = a.uv.fs_open(path, "r", 438)
assert(not err, err)
local err, stat = a.uv.fs_fstat(fd)
assert(not err, err)
local err, data = a.uv.fs_read(fd, stat.size, 0)
assert(not err, err)
local err = a.uv.fs_close(fd)
assert(not err, err)
return data
end
```
#### Plugins using this
- [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim)
- [gitsigns.nvim](https://github.com/lewis6991/gitsigns.nvim)
- [vgit.nvim](https://github.com/tanvirtin/vgit.nvim)
- [neogit](https://github.com/TimUntersberger/neogit)
### plenary.async_lib
Please use `plenary.async` instead. This was version 1 and is just here for compatibility reasons.
### plenary.job
A Lua module to interact with system processes. Pass in your `command`, the desired `args`, `env` and `cwd`.
Define optional callbacks for `on_stdout`, `on_stderr` and `on_exit` and `start` your Job.
Note: Each job has an empty environment.
```lua
local Job = require'plenary.job'
Job:new({
command = 'rg',
args = { '--files' },
cwd = '/usr/bin',
env = { ['a'] = 'b' },
on_exit = function(j, return_val)
print(return_val)
print(j:result())
end,
}):sync() -- or start()
```
### plenary.path
A Lua module that implements a bunch of the things from `pathlib` from Python, so that paths are easy to work with.
### plenary.scandir
`plenery.scandir` is fast recursive file operations. It is similar to unix `find` or `fd` in that it can do recursive scans over a given directory, or a set of directories.
It offers a wide range of opts for limiting the depth, show hidden and more. `plenary.scan_dir` can be ran synchronously and asynchronously and offers `on_insert(file, typ)` and `on_exit(files)` callbacks. `on_insert(file, typ)` is available for both while `on_exit(files)` is only available for async.
```lua
local scan = require'plenary.scandir`
scan.scan_dir('.', { hidden = true, depth = 2 })
```
This module also offers `ls -la` sync and async functions that will return a formated string for all files in the directory.
Why? Just for fun
### plenary.context_manager
Implements `with` and `open` just like in Python. For example:
```lua
local with = context_manager.with
local open = context_manager.open
local result = with(open("README.md"), function(reader)
return reader:read()
end)
assert(result == "# plenary.nvim")
```
### plenary.test_harness
Supports (simple) busted-style testing. It implements a mock-ed busted interface, that will allow you to run simple
busted style tests in separate neovim instances.
To run the current spec file in a floating window, you can use the keymap `<Plug>PlenaryTestFile`. For example:
```
nmap <leader>t <Plug>PlenaryTestFile
```
To run a whole directory from the command line, you could do something like:
```
nvim --headless -c "PlenaryBustedDirectory tests/plenary/ {minimal_init = 'tests/minimal_init.vim'}"
```
Where the first argument is the directory you'd like to test. It will search for files with
the pattern `*_spec.lua` and execute them in separate neovim instances.
The second argument is a Lua option table with the following fields:
- `minimal_init`: specify an init.vim to use for this instance, uses `--noplugin`
- `minimal`: uses `--noplugin` without an init script (overrides `minimal_init`)
- `sequential`: whether to run tests sequentially (default is to run in parallel)
- `keep_going`: if `sequential`, whether to continue on test failure (default true)
- `timeout`: controls the maximum time allotted to each job in parallel or
sequential operation (defaults to 50,000 milliseconds)
The exit code is 0 when success and 1 when fail, so you can use it easily in a `Makefile`!
NOTE:
So far, the only supported busted items are:
- `describe`
- `it`
- `pending`
- `before_each`
- `after_each`
- `clear`
- `assert.*` etc. (from luassert, which is bundled)
OTHER NOTE:
We used to support `luaunit` and original `busted` but it turns out it was way too hard and not worthwhile
for the difficulty of getting them setup, particularly on other platforms or in CI. Now, we have a dep free
(or at least, no other installation steps necessary) `busted` implementation that can be used more easily.
Please take a look at the new APIs and make any issues for things that aren't clear. I am happy to fix them
and make it work well :)
OTHER OTHER NOTE:
Take a look at some test examples [here](TESTS_README.md).
#### Colors
You no longer need nvim-terminal to get this to work. We use `nvim_open_term` now.
### plenary.filetype
Will detect the filetype based on `extension`/`special filename`/`shebang` or `modeline`
- `require'plenary.filetype'.detect(filepath, opts)` is a function that does all of above and exits as soon as a filetype is found
- `require'plenary.filetype'.detect_from_extension(filepath)`
- `require'plenary.filetype'.detect_from_name(filepath)`
- `require'plenary.filetype'.detect_from_modeline(filepath)`
- `require'plenary.filetype'.detect_from_shebang(filepath)`
Add filetypes by creating a new file named `~/.config/nvim/data/plenary/filetypes/foo.lua` and register that file with
`:lua require'plenary.filetype'.add_file('foo')`. Content of the file should look like that:
```lua
return {
extension = {
-- extension = filetype
-- example:
['jl'] = 'julia',
},
file_name = {
-- special filenames, likes .bashrc
-- we provide a decent amount
-- name = filetype
-- example:
['.bashrc'] = 'bash',
},
shebang = {
-- Shebangs are supported as well. Currently we provide
-- sh, bash, zsh, python, perl with different prefixes like
-- /usr/bin, /bin/, /usr/bin/env, /bin/env
-- shebang = filetype
-- example:
['/usr/bin/node'] = 'javascript',
}
}
```
### plenary.strings
Re-implement VimL funcs to use them in Lua loop.
* `strings.strdisplaywidth`
* `strings.strcharpart`
And some other funcs are here to deal with common problems.
* `strings.truncate`
* `strings.align_str`
* `strings.dedent`
### plenary.profile
Thin wrapper around LuaJIT's [`jit.p` profiler](https://blast.hk/moonloader/luajit/ext_profiler.html).
```lua
require'plenary.profile'.start("profile.log")
-- code to be profiled
require'plenary.profile'.stop()
```
You can use `start("profile.log", {flame = true})` to output the log in a
flamegraph-compatible format. A flamegraph can be created from this using
https://github.com/jonhoo/inferno via
```
inferno-flamegraph profile.log > flame.svg
```
The resulting interactive SVG file can be viewed in any browser.
Status: WIP
### plenary.popup
See [popup documentation](./POPUP.md) for both progress tracking and implemented APIs.
### plenary.window
Window helper functions to wrap some of the more difficult cases. Particularly for floating windows.
Status: WIP
### plenary.collections
Contains pure lua implementations for various standard collections.
```lua
local List = require 'plenary.collections.py_list'
local myList = List { 9, 14, 32, 5 }
for i, v in myList:iter() do
print(i, v)
end
```
Status: WIP
### Troubleshooting
If you're having trouble / things are hanging / other problems:
```
$ export DEBUG_PLENARY=true
```
This will enable debuggin for the plugin.
### plenary.neorocks
DELETED: Please use packer.nvim or other lua-rocks wrapper instead. This no longer exists.
### FAQ
1. Error: Too many open files
- \*nix systems have a setting to configure the maximum amount of open file
handles. It can occur that the default value is pretty low and that you end
up getting this error after opening a couple of files. You can see the
current limit with `ulimit -n` and set it with `ulimit -n 4096`. (macos might
work different)

View File

@ -0,0 +1,118 @@
# Testing Guide
Some testing examples using Plenary.nvim
# A simple test
This tests demonstrates a **describe** block that contains two tests defined with **it** blocks, the describe block also contains a **before_each** call that gets called before each test.
```lua
describe("some basics", function()
local bello = function(boo)
return "bello " .. boo
end
local bounter
before_each(function()
bounter = 0
end)
it("some test", function()
bounter = 100
assert.equals("bello Brian", bello("Brian"))
end)
it("some other test", function()
assert.equals(0, bounter)
end)
end)
```
The test **some test** checks that a functions output is as expected based on the input. The second test **some other test** checks that the variable **bounter** is reset for each test (as defined in the before_each block).
# mocking with luassert
Plenary.nvim comes bundled with [luassert](https://github.com/Olivine-Labs/luassert) a library that's built to extend the built-int assertions... but it also comes with stubs, mocks and spies!
Sometimes it's useful to test functions that have nvim api function calls within them, take for example the following example of a simple module that creates a new buffer and opens in it in a split.
**module.lua**
```lua
local M = {}
function M.realistic_func()
local buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_command("sbuffer " .. buf)
end
return M
```
The following is an example of completely mocking a module, and another of just stubbing a single function within a module. In this case the module is `vim.api`, with an aim of giving an example of a unit test (fully mocked) and an integration test... details in the comments.
**module.lua**
```lua
-- import the luassert.mock module
local mock = require('luassert.mock')
local stub = require('luassert.stub')
describe("example", function()
-- instance of module to be tested
local testModule = require('example.module')
-- mocked instance of api to interact with
describe("realistic_func", function()
it("Should make expected calls to api, fully mocked", function()
-- mock the vim.api
local api = mock(vim.api, true)
-- set expectation when mocked api call made
api.nvim_create_buf.returns(5)
testModule.realistic_func()
-- assert api was called with expcted values
assert.stub(api.nvim_create_buf).was_called_with(false, true)
-- assert api was called with set expectation
assert.stub(api.nvim_command).was_called_with("sbuffer 5")
-- revert api back to it's former glory
mock.revert(api)
end)
it("Should mock single api call", function()
-- capture some number of windows and buffers before
-- running our function
local buf_count = #vim.api.nvim_list_bufs()
local win_count = #vim.api.nvim_list_wins()
-- stub a single function in the api
stub(vim.api, "nvim_command")
testModule.realistic_func()
-- capture some details after running out function
local after_buf_count = #vim.api.nvim_list_bufs()
local after_win_count = #vim.api.nvim_list_wins()
-- why 3 not two? NO IDEA! The point is we mocked
-- nvim_commad and there is only a single window
assert.equals(3, buf_count)
assert.equals(4, after_buf_count)
-- WOOPIE!
assert.equals(1, win_count)
assert.equals(1, after_win_count)
end)
end)
end)
```
To test this in your `~/.config/nvim` configuration, try the suggested file structure:
```
lua/example/module.lua
lua/spec/example/module_spec.lua
```

View File

@ -0,0 +1,885 @@
return {
extension = {
['ncl'] = [[text]],
['ph'] = [[perl]],
['al'] = [[perl]],
['cl'] = [[lisp]],
['vhi'] = [[vhdl]],
['sublime-snippet'] = [[xml]],
['tcl'] = [[tcl]],
['pp'] = [[pascal]],
['builds'] = [[xml]],
['lua'] = [[lua]],
['pkb'] = [[plsql]],
['wl'] = [[mma]],
['6'] = [[groff]],
['scm'] = [[scheme]],
['ml'] = [[ocaml]],
['filters'] = [[xml]],
['st'] = [[html]],
['ksy'] = [[yaml]],
['mt'] = [[mma]],
['ada'] = [[ada]],
['vho'] = [[vhdl]],
['nawk'] = [[awk]],
['3pm'] = [[groff]],
['maxhelp'] = [[json]],
['ct'] = [[xml]],
['ipp'] = [[cpp]],
['a51'] = [[asm]],
['meta'] = [[yaml]],
['fsproj'] = [[xml]],
['ccxml'] = [[xml]],
['ado'] = [[stata]],
['mli'] = [[ocaml]],
['r2'] = [[rebol]],
['csl'] = [[xml]],
['bbx'] = [[tex]],
['xsjslib'] = [[javascript]],
['e'] = [[eiffel]],
['tac'] = [[python]],
['mustache'] = [[smarty]],
['zcml'] = [[xml]],
['glade'] = [[xml]],
['cuh'] = [[cuda]],
['cxx'] = [[cpp]],
['urdf'] = [[xml]],
['ditaval'] = [[xml]],
['cscfg'] = [[xml]],
['j2'] = [[django]],
['pkgproj'] = [[xml]],
['ma'] = [[mma]],
['cljs.hl'] = [[clojure]],
['brd'] = [[xml]],
['asn'] = [[asn]],
['xspec'] = [[xml]],
['db2'] = [[sql]],
['sjs'] = [[javascript]],
['m'] = [[matlab]],
['gdbinit'] = [[gdb]],
['re'] = [[cpp]],
['adoc'] = [[asciidoc]],
['pyde'] = [[python]],
['mjml'] = [[xml]],
['workbook'] = [[markdown]],
['ssjs'] = [[javascript]],
['dircolors'] = [[dircolors]],
['ui'] = [[xml]],
['ant'] = [[xml]],
['wast'] = [[wast]],
['_js'] = [[javascript]],
['7'] = [[groff]],
['dylan'] = [[dylan]],
['jsproj'] = [[xml]],
['jsb'] = [[javascript]],
['xml'] = [[xml]],
['tcsh'] = [[tcsh]],
['xpy'] = [[python]],
['wisp'] = [[clojure]],
['tm'] = [[tcl]],
['plot'] = [[gnuplot]],
['mjs'] = [[javascript]],
['cjs'] = [[javascript]],
['env'] = [[sh]],
['jsfl'] = [[javascript]],
['eye'] = [[ruby]],
['jinja2'] = [[django]],
['lookml'] = [[yaml]],
['mcmeta'] = [[json]],
['workflow'] = [[xml]],
['eq'] = [[cs]],
['storyboard'] = [[xml]],
['nbp'] = [[mma]],
['maxpat'] = [[json]],
['yy'] = [[yacc]],
['h++'] = [[cpp]],
['asc'] = [[asciidoc]],
['phps'] = [[php]],
['cabal'] = [[cabal]],
['xacro'] = [[xml]],
['dhall'] = [[haskell]],
['vba'] = [[vim]],
['feature'] = [[cucumber]],
['psc1'] = [[xml]],
['plb'] = [[plsql]],
['logtalk'] = [[logtalk]],
['pascal'] = [[pascal]],
['smk'] = [[python]],
['rst.txt'] = [[rst]],
['svh'] = [[systemverilog]],
['sthlp'] = [[stata]],
['nb'] = [[text]],
['hrl'] = [[erlang]],
['f'] = [[forth]],
['rb'] = [[ruby]],
['xsd'] = [[xml]],
['cfg'] = [[dosini]],
['1in'] = [[groff]],
['mkiv'] = [[tex]],
['mask'] = [[yaml]],
['cproject'] = [[xml]],
['gnuplot'] = [[gnuplot]],
['sagews'] = [[python]],
['nsh'] = [[nsis]],
['njk'] = [[django]],
['pic'] = [[pic]],
['man'] = [[groff]],
['owl'] = [[xml]],
['ino'] = [[cpp]],
['dtx'] = [[tex]],
['volt'] = [[d]],
['ltx'] = [[tex]],
['rtf'] = [[rtf]],
['jscad'] = [[javascript]],
['8'] = [[groff]],
['3qt'] = [[groff]],
['n'] = [[groff]],
['lisp'] = [[lisp]],
['forth'] = [[forth]],
['dll.config'] = [[xml]],
['prefab'] = [[yaml]],
['ads'] = [[ada]],
['jake'] = [[javascript]],
['zsh'] = [[sh]],
['v'] = [[verilog]],
['ini'] = [[dosini]],
['command'] = [[sh]],
['fr'] = [[forth]],
['yacc'] = [[yacc]],
['ccproj'] = [[xml]],
['xqy'] = [[xquery]],
['rabl'] = [[ruby]],
['nr'] = [[groff]],
['wsgi'] = [[python]],
['model.lkml'] = [[yaml]],
['lsl'] = [[lsl]],
['yyp'] = [[json]],
['yaml-tmlanguage'] = [[yaml]],
['rbi'] = [[ruby]],
['srt'] = [[lisp]],
['unity'] = [[yaml]],
['vhs'] = [[vhdl]],
['yap'] = [[prolog]],
['php5'] = [[php]],
['pas'] = [[pascal]],
['gyp'] = [[python]],
['jsonl'] = [[json]],
['cc'] = [[cpp]],
['grt'] = [[groovy]],
['txi'] = [[texinfo]],
['asm'] = [[asm]],
['plx'] = [[perl]],
['prefs'] = [[dosini]],
['mly'] = [[ocaml]],
['xsl'] = [[xslt]],
['osm'] = [[xml]],
['sc'] = [[scala]],
['au3'] = [[autoit]],
['obj'] = [[obj]],
['uc'] = [[java]],
['lhs'] = [[lhaskell]],
['1'] = [[groff]],
['resx'] = [[xml]],
['geojson'] = [[json]],
['tmux'] = [[sh]],
['rg'] = [[clojure]],
['tcc'] = [[cpp]],
['cbl'] = [[cobol]],
['wat'] = [[wast]],
['com'] = [[dcl]],
['podspec'] = [[ruby]],
['no'] = [[text]],
['spec'] = [[python]],
['vhw'] = [[vhdl]],
['maxproj'] = [[json]],
['xbm'] = [[c]],
['wxl'] = [[xml]],
['mk'] = [[make]],
['epj'] = [[json]],
['do'] = [[stata]],
['rexx'] = [[rexx]],
['ms'] = [[groff]],
['w'] = [[cweb]],
['ss'] = [[scheme]],
['desktop.in'] = [[desktop]],
['po'] = [[po]],
['mdpolicy'] = [[xml]],
['mathematica'] = [[mma]],
['vw'] = [[plsql]],
['rss'] = [[xml]],
['cs'] = [[cs]],
['mkdown'] = [[markdown]],
['gs'] = [[javascript]],
['doh'] = [[stata]],
['vbproj'] = [[xml]],
['sfproj'] = [[xml]],
['mat'] = [[yaml]],
['cmake.in'] = [[cmake]],
['rest.txt'] = [[rst]],
['sls'] = [[scheme]],
['fxml'] = [[xml]],
['jss'] = [[javascript]],
['avsc'] = [[json]],
['mbox'] = [[basic]],
['cake'] = [[cs]],
['gst'] = [[xml]],
['p6l'] = [[perl6]],
['cdf'] = [[mma]],
['pyi'] = [[python]],
['ahk'] = [[autohotkey]],
['tmac'] = [[groff]],
['xaml'] = [[xml]],
['texi'] = [[texinfo]],
['udf'] = [[sql]],
['rebol'] = [[rebol]],
['gvy'] = [[groovy]],
['h'] = [[c]],
['pm6'] = [[perl6]],
['ivy'] = [[xml]],
['properties'] = [[dosini]],
['eliomi'] = [[ocaml]],
['nanorc'] = [[nanorc]],
['sps'] = [[scheme]],
['nproj'] = [[xml]],
['ch'] = [[clipper]],
['odd'] = [[xml]],
['fun'] = [[sml]],
['mir'] = [[yaml]],
['nuspec'] = [[xml]],
['pck'] = [[plsql]],
['2'] = [[groff]],
['nl'] = [[lisp]],
['fth'] = [[forth]],
['cps'] = [[pascal]],
['sh'] = [[sh]],
['rbw'] = [[ruby]],
['dlm'] = [[idl]],
['pod'] = [[pod]],
['clixml'] = [[xml]],
['duby'] = [[ruby]],
['gdb'] = [[gdb]],
['boot'] = [[clojure]],
['adb'] = [[ada]],
['tex'] = [[tex]],
['csx'] = [[cs]],
['sbt'] = [[scala]],
['csdef'] = [[xml]],
['dpr'] = [[pascal]],
['rex'] = [[rexx]],
['srdf'] = [[xml]],
['pl'] = [[perl]],
['markdown'] = [[markdown]],
['cgi'] = [[perl]],
['cp'] = [[cpp]],
['jinja'] = [[django]],
['gp'] = [[gnuplot]],
['x'] = [[rpcgen]],
['pt'] = [[xml]],
['tpp'] = [[cpp]],
['intr'] = [[dylan]],
['JSON-tmLanguage'] = [[json]],
['ux'] = [[xml]],
['fpp'] = [[fortran]],
['phpt'] = [[php]],
['4th'] = [[forth]],
['dita'] = [[xml]],
['viw'] = [[sql]],
['vsixmanifest'] = [[xml]],
['clj'] = [[clojure]],
['yml.mysql'] = [[yaml]],
['hpp'] = [[cpp]],
['xsp.metadata'] = [[xml]],
['sexp'] = [[lisp]],
['csproj'] = [[xml]],
['tab'] = [[sql]],
['cql'] = [[sql]],
['abap'] = [[abap]],
['wlua'] = [[lua]],
['lex'] = [[lex]],
['java'] = [[java]],
['edn'] = [[clojure]],
['ditamap'] = [[xml]],
['3p'] = [[groff]],
['xul'] = [[xml]],
['cmake'] = [[cmake]],
['kit'] = [[basic]],
['rs.in'] = [[rust]],
['php4'] = [[php]],
['hxx'] = [[cpp]],
['cljscm'] = [[clojure]],
['vcxproj'] = [[xml]],
['php3'] = [[php]],
['xml.dist'] = [[xml]],
['ged'] = [[gedcom]],
['i'] = [[asm]],
['xproc'] = [[xml]],
['ndproj'] = [[xml]],
['less'] = [[less]],
['lgt'] = [[logtalk]],
['sql'] = [[plsql]],
['cls'] = [[tex]],
['targets'] = [[xml]],
['me'] = [[groff]],
['3x'] = [[groff]],
['gtpl'] = [[groovy]],
['prg'] = [[clipper]],
['natvis'] = [[xml]],
['3'] = [[groff]],
['di'] = [[d]],
['1x'] = [[groff]],
['mdoc'] = [[groff]],
['xsjs'] = [[javascript]],
['iml'] = [[xml]],
['ctp'] = [[php]],
['kml'] = [[xml]],
['eml'] = [[basic]],
['raml'] = [[raml]],
['gml'] = [[xml]],
['yml'] = [[yaml]],
['xq'] = [[xquery]],
['sml'] = [[sml]],
['sublime-syntax'] = [[yaml]],
['mm'] = [[xml]],
['y'] = [[yacc]],
['mu'] = [[mupad]],
['sld'] = [[scheme]],
['asd'] = [[lisp]],
['pgsql'] = [[sql]],
['nqp'] = [[perl6]],
['anim'] = [[yaml]],
['vhf'] = [[vhdl]],
['cu'] = [[cuda]],
['make'] = [[make]],
['pyx'] = [[pyrex]],
['c++'] = [[cpp]],
['lslp'] = [[lsl]],
['rake'] = [[ruby]],
['webmanifest'] = [[json]],
['ijs'] = [[j]],
['hsc'] = [[haskell]],
['pl6'] = [[perl6]],
['p6m'] = [[perl6]],
['ny'] = [[lisp]],
['mak'] = [[make]],
['p6'] = [[perl6]],
['6pm'] = [[perl6]],
['lfe'] = [[lisp]],
['6pl'] = [[perl6]],
['toc'] = [[tex]],
['yrl'] = [[erlang]],
['vssettings'] = [[xml]],
['sty'] = [[tex]],
['ipynb'] = [[json]],
['mkvi'] = [[tex]],
['p'] = [[gnuplot]],
['lmi'] = [[python]],
['rockspec'] = [[lua]],
['vhd'] = [[vhdl]],
['sieve'] = [[sieve]],
['lbx'] = [[tex]],
['1m'] = [[groff]],
['jelly'] = [[xml]],
['3m'] = [[groff]],
['ins'] = [[tex]],
['xib'] = [[xml]],
['cbx'] = [[tex]],
['pd_lua'] = [[lua]],
['dae'] = [[xml]],
['emacs.desktop'] = [[lisp]],
['bison'] = [[yacc]],
['matlab'] = [[matlab]],
['emacs'] = [[lisp]],
['pot'] = [[po]],
['el'] = [[lisp]],
['podsl'] = [[lisp]],
['pac'] = [[javascript]],
['gitignore'] = [[gitignore]],
['vue'] = [[vue]],
['html.hl'] = [[html]],
['trg'] = [[plsql]],
['tps'] = [[plsql]],
['hs-boot'] = [[haskell]],
['csh'] = [[tcsh]],
['mawk'] = [[awk]],
['fan'] = [[fan]],
['pike'] = [[pike]],
['story'] = [[cucumber]],
['plsql'] = [[plsql]],
['app.src'] = [[erlang]],
['mkd'] = [[markdown]],
['ksh'] = [[sh]],
['inl'] = [[cpp]],
['ml4'] = [[ocaml]],
['f77'] = [[fortran]],
['pls'] = [[plsql]],
['opencl'] = [[c]],
['proj'] = [[xml]],
['4'] = [[groff]],
['wsf'] = [[xml]],
['ninja'] = [[ninja]],
['rest'] = [[rst]],
['tpb'] = [[plsql]],
['xquery'] = [[xquery]],
['gitconfig'] = [[gitconfig]],
['r3'] = [[rebol]],
['reb'] = [[rebol]],
['gawk'] = [[awk]],
['groovy'] = [[groovy]],
['auk'] = [[awk]],
['r'] = [[rebol]],
['cmd'] = [[dosbatch]],
['awk'] = [[awk]],
['xslt'] = [[xslt]],
['ps1xml'] = [[xml]],
['spc'] = [[plsql]],
['wixproj'] = [[xml]],
['upc'] = [[c]],
['hic'] = [[clojure]],
['cljx'] = [[clojure]],
['cljs'] = [[clojure]],
['json'] = [[json]],
['cljc'] = [[clojure]],
['pfa'] = [[postscr]],
['vhdl'] = [[vhdl]],
['cl2'] = [[clojure]],
['nsi'] = [[nsis]],
['g4'] = [[antlr]],
['sage'] = [[python]],
['haml.deface'] = [[haml]],
['haml'] = [[haml]],
['rno'] = [[groff]],
['xrl'] = [[erlang]],
['escript'] = [[erlang]],
['pks'] = [[plsql]],
['grxml'] = [[xml]],
['erl'] = [[erlang]],
['chem'] = [[pic]],
['eb'] = [[python]],
['patch'] = [[diff]],
['diff'] = [[diff]],
['mspec'] = [[ruby]],
['xsp-config'] = [[xml]],
['gv'] = [[dot]],
['adp'] = [[tcl]],
['webapp'] = [[json]],
['epsi'] = [[postscr]],
['dot'] = [[dot]],
['phtml'] = [[php]],
['lid'] = [[dylan]],
['cpy'] = [[cobol]],
['mkdn'] = [[markdown]],
['ccp'] = [[cobol]],
['cob'] = [[cobol]],
['glf'] = [[tcl]],
['sass'] = [[sass]],
['gnu'] = [[gnuplot]],
['pyp'] = [[python]],
['gsp'] = [[gsp]],
['asn1'] = [[asn]],
['sig'] = [[sml]],
['t'] = [[perl]],
['xlf'] = [[xml]],
['perl'] = [[perl]],
['rpy'] = [[python]],
['jbuilder'] = [[ruby]],
['mdwn'] = [[markdown]],
['dfm'] = [[pascal]],
['c'] = [[c]],
['pyw'] = [[python]],
['mkfile'] = [[make]],
['py3'] = [[python]],
['gypi'] = [[python]],
['py'] = [[python]],
['watchr'] = [[ruby]],
['thor'] = [[ruby]],
['ruby'] = [[ruby]],
['ru'] = [[ruby]],
['gltf'] = [[json]],
['rbx'] = [[ruby]],
['rbuild'] = [[ruby]],
['m4'] = [[m4]],
['god'] = [[ruby]],
['har'] = [[json]],
['sas'] = [[sas]],
['mkii'] = [[tex]],
['frt'] = [[forth]],
['icl'] = [[clean]],
['mirah'] = [[ruby]],
['wxi'] = [[xml]],
['lektorproject'] = [[dosini]],
['nasm'] = [[asm]],
['njs'] = [[javascript]],
['vstemplate'] = [[xml]],
['jsm'] = [[javascript]],
['html'] = [[html]],
['xpm'] = [[xpm]],
['tool'] = [[sh]],
['rdf'] = [[xml]],
['rd'] = [[r]],
['dyl'] = [[dylan]],
['p8'] = [[lua]],
['prw'] = [[clipper]],
['es6'] = [[javascript]],
['gmx'] = [[xml]],
['pluginspec'] = [[ruby]],
['gemspec'] = [[ruby]],
['aux'] = [[tex]],
['lvlib'] = [[xml]],
['cats'] = [[c]],
['fcgi'] = [[perl]],
['for'] = [[forth]],
['scxml'] = [[xml]],
['mdown'] = [[markdown]],
['scala'] = [[scala]],
['pxd'] = [[pyrex]],
['mxml'] = [[xml]],
['chs'] = [[haskell]],
['syntax'] = [[yaml]],
['5'] = [[groff]],
['xliff'] = [[xml]],
['pyt'] = [[python]],
['view.lkml'] = [[yaml]],
['pat'] = [[json]],
['bash'] = [[sh]],
['tfstate'] = [[json]],
['vmb'] = [[vim]],
['prolog'] = [[prolog]],
['asciidoc'] = [[asciidoc]],
['asset'] = [[yaml]],
['mxt'] = [[json]],
['rviz'] = [[yaml]],
['yaml.sed'] = [[yaml]],
['inc'] = [[php]],
['props'] = [[xml]],
['psgi'] = [[perl]],
['axml'] = [[xml]],
['pxi'] = [[pyrex]],
['admx'] = [[xml]],
['nse'] = [[lua]],
['wxs'] = [[xml]],
['ice'] = [[json]],
['builder'] = [[ruby]],
['jsp'] = [[jsp]],
['eliom'] = [[ocaml]],
['go'] = [[go]],
['ihlp'] = [[stata]],
['scss'] = [[scss]],
['sce'] = [[scilab]],
['lsp'] = [[lisp]],
['x3d'] = [[xml]],
['rs'] = [[rust]],
['php'] = [[php]],
['htm'] = [[html]],
['pprx'] = [[rexx]],
['es'] = [[erlang]],
['js'] = [[javascript]],
['ts'] = [[typescript]],
['uno'] = [[cs]],
['sch'] = [[scheme]],
['lvproj'] = [[xml]],
['xs'] = [[xs]],
['pro'] = [[idl]],
['dotsettings'] = [[xml]],
['res'] = [[xml]],
['xmi'] = [[xml]],
['ahkl'] = [[autohotkey]],
['plt'] = [[gnuplot]],
['wlt'] = [[mma]],
['lpr'] = [[pascal]],
['dof'] = [[dosini]],
['fs'] = [[forth]],
['sh.in'] = [[sh]],
['bat'] = [[dosbatch]],
['roff'] = [[groff]],
['depproj'] = [[xml]],
['mdx'] = [[markdown]],
['hs'] = [[haskell]],
['frag'] = [[javascript]],
['ps'] = [[postscr]],
['9'] = [[groff]],
['eps'] = [[postscr]],
['ck'] = [[java]],
['rst'] = [[rst]],
['css'] = [[css]],
['aw'] = [[php]],
['veo'] = [[verilog]],
['adml'] = [[xml]],
['mll'] = [[ocaml]],
['tst'] = [[scilab]],
['svg'] = [[svg]],
['bdy'] = [[plsql]],
['xql'] = [[xquery]],
['xqm'] = [[xquery]],
['pm'] = [[perl]],
['texinfo'] = [[texinfo]],
['prc'] = [[plsql]],
['3in'] = [[groff]],
['rbxs'] = [[lua]],
['xproj'] = [[xml]],
['bdf'] = [[bdf]],
['ampl'] = [[ampl]],
['d'] = [[d]],
['fnc'] = [[plsql]],
['cobol'] = [[cobol]],
['txt'] = [[text]],
['vim'] = [[vim]],
['mata'] = [[stata]],
['linq'] = [[cs]],
['launch'] = [[xml]],
['sv'] = [[systemverilog]],
['nlogo'] = [[lisp]],
['sci'] = [[scilab]],
['dockerfile'] = [[dockerfile]],
['vht'] = [[vhdl]],
['sed'] = [[sed]],
['xht'] = [[html]],
['liquid'] = [[liquid]],
['latte'] = [[latte]],
['vhost'] = [[apache]],
['matah'] = [[stata]],
['ronn'] = [[markdown]],
['tpl'] = [[smarty]],
['xhtml'] = [[html]],
['bones'] = [[javascript]],
['bats'] = [[sh]],
['xpl'] = [[xml]],
['shproj'] = [[xml]],
['tfstate.backup'] = [[json]],
['bzl'] = [[bzl]],
['cpp'] = [[cpp]],
['mod'] = [[ampl]],
['idc'] = [[c]],
['hh'] = [[cpp]],
['druby'] = [[ruby]],
['apacheconf'] = [[apache]],
['l'] = [[lex]],
['md'] = [[markdown]],
['vxml'] = [[xml]],
['pmod'] = [[pike]],
['wsdl'] = [[xml]],
['mtml'] = [[basic]],
['vh'] = [[systemverilog]],
['ddl'] = [[plsql]],
['topojson'] = [[json]],
['mysql'] = [[sql]],
['kojo'] = [[scala]],
['rsx'] = [[r]],
['tml'] = [[xml]],
['dcl'] = [[clean]],
['reek'] = [[yaml]],
['yaml'] = [[yaml]],
['desktop'] = [[desktop]],
['tsx'] = [[xml]],
},
file_name = {
['.classpath'] = [[xml]],
['bsdmakefile'] = [[make]],
['delete.me'] = [[text]],
['packages.config'] = [[xml]],
['jenkinsfile'] = [[groovy]],
['ant.xml'] = [[ant]],
['makefile.frag'] = [[make]],
['puppetfile'] = [[ruby]],
['.inputrc'] = [[readline]],
['.zshrc'] = [[sh]],
['inputrc'] = [[readline]],
['gnumakefile'] = [[make]],
['makefile.sco'] = [[make]],
['pkgbuild'] = [[sh]],
['.babelignore'] = [[gitignore]],
['.bash_logout'] = [[sh]],
['.nanorc'] = [[nanorc]],
['berksfile'] = [[ruby]],
['lexer.x'] = [[lex]],
['sconscript'] = [[python]],
['makefile.boot'] = [[make]],
['starfield'] = [[tcl]],
['.login'] = [[sh]],
['composer.lock'] = [[json]],
['dir_colors'] = [[dircolors]],
['.nvimrc'] = [[vim]],
['.project'] = [[xml]],
['web.config'] = [[xml]],
['makefile.in'] = [[make]],
['readme.me'] = [[text]],
['.cproject'] = [[xml]],
['nuget.config'] = [[xml]],
['zlogin'] = [[sh]],
['phakefile'] = [[php]],
['brewfile'] = [[ruby]],
['podfile'] = [[ruby]],
['use.stable.mask'] = [[text]],
['.gemrc'] = [[yaml]],
['emakefile'] = [[erlang]],
['notebook'] = [[json]],
['read.me'] = [[text]],
['.dircolors'] = [[dircolors]],
['.gitmodules'] = [[gitconfig]],
['owh'] = [[tcl]],
['zlogout'] = [[sh]],
['package.use.stable.mask'] = [[text]],
['httpd.conf'] = [[apache]],
['buildozer.spec'] = [[dosini]],
['.zshenv'] = [[sh]],
['workspace'] = [[bzl]],
['readme.1st'] = [[text]],
['.simplecov'] = [[ruby]],
['.vimrc'] = [[vim]],
['.htmlhintrc'] = [[json]],
['mmn'] = [[groff]],
['rexfile'] = [[perl]],
['m3overrides'] = [[quake]],
['.arcconfig'] = [[json]],
['.htaccess'] = [[apache]],
['.php_cs'] = [[php]],
['tiltfile'] = [[bzl]],
['wscript'] = [[python]],
['.stylelintignore'] = [[gitignore]],
['gvimrc'] = [[vim]],
['.tern-config'] = [[json]],
['_dir_colors'] = [[dircolors]],
['app.config'] = [[xml]],
['abbrev_defs'] = [[lisp]],
['_emacs'] = [[lisp]],
['buck'] = [[bzl]],
['dockerfile'] = [[dockerfile]],
['project.ede'] = [[lisp]],
['thorfile'] = [[ruby]],
['rakefile'] = [[ruby]],
['.viper'] = [[lisp]],
['.spacemacs'] = [[lisp]],
['.gnus'] = [[lisp]],
['snapfile'] = [[ruby]],
['eqnrc'] = [[groff]],
['_dircolors'] = [[dircolors]],
['configure.ac'] = [[m4]],
['cabal.project'] = [[cabal]],
['.env'] = [[sh]],
['.luacheckrc'] = [[lua]],
['9fs'] = [[sh]],
['cabal.config'] = [[cabal]],
['.bash_aliases'] = [[sh]],
['install.mysql'] = [[text]],
['yarn.lock'] = [[yaml]],
['gitignore_global'] = [[gitignore]],
['.cshrc'] = [[sh]],
['.bash_history'] = [[sh]],
['.env.example'] = [[sh]],
['.npmignore'] = [[gitignore]],
['gitignore-global'] = [[gitignore]],
['build.bazel'] = [[bzl]],
['.flaskenv'] = [[sh]],
['license'] = [[text]],
['mavenfile'] = [[ruby]],
['.vscodeignore'] = [[gitignore]],
['.abbrev_defs'] = [[lisp]],
['gemfile.lock'] = [[ruby]],
['go.mod'] = [[text]],
['.gvimrc'] = [[vim]],
['.nodemonignore'] = [[gitignore]],
['bashrc'] = [[sh]],
['man'] = [[sh]],
['.dockerignore'] = [[gitignore]],
['makefile.am'] = [[make]],
['.zlogin'] = [[sh]],
['.cvsignore'] = [[gitignore]],
['.coffeelintignore'] = [[gitignore]],
['.php'] = [[php]],
['expr-dist'] = [[r]],
['zshrc'] = [[sh]],
['.clang-format'] = [[yaml]],
['bash_aliases'] = [[sh]],
['.exrc'] = [[vim]],
['web.release.config'] = [[xml]],
['.atomignore'] = [[gitignore]],
['makefile.wat'] = [[make]],
['troffrc-end'] = [[groff]],
['vimrc'] = [[vim]],
['cmakelists.txt'] = [[cmake]],
['.gitconfig'] = [[gitconfig]],
['riemann.config'] = [[clojure]],
['zprofile'] = [[sh]],
['rebar.lock'] = [[erlang]],
['news'] = [[text]],
['rebar.config'] = [[erlang]],
['mcmod.info'] = [[json]],
['cpanfile'] = [[perl]],
['readme.mysql'] = [[text]],
['makefile.pl'] = [[perl]],
['dangerfile'] = [[ruby]],
['snakefile'] = [[python]],
['contents.lr'] = [[markdown]],
['sconstruct'] = [[python]],
['glide.lock'] = [[yaml]],
['deps'] = [[python]],
['.zprofile'] = [[sh]],
['.gclient'] = [[python]],
['keep.me'] = [[text]],
['troffrc'] = [[groff]],
['m3makefile'] = [[quake]],
['cshrc'] = [[sh]],
['.emacs.desktop'] = [[lisp]],
['.zlogout'] = [[sh]],
['.rprofile'] = [[r]],
['cask'] = [[lisp]],
['jarfile'] = [[ruby]],
['deliverfile'] = [[ruby]],
['copyright.regex'] = [[text]],
['.prettierignore'] = [[gitignore]],
['gemfile'] = [[ruby]],
['profile'] = [[sh]],
['fastfile'] = [[ruby]],
['guardfile'] = [[ruby]],
['capfile'] = [[ruby]],
['buildfile'] = [[ruby]],
['jakefile'] = [[javascript]],
['appraisals'] = [[ruby]],
['fontlog'] = [[text]],
['package.use.mask'] = [[text]],
['.pryrc'] = [[ruby]],
['.emacs'] = [[lisp]],
['.watchmanconfig'] = [[json]],
['.irbrc'] = [[ruby]],
['.tern-project'] = [[json]],
['web.debug.config'] = [[xml]],
['.eslintignore'] = [[gitignore]],
['.gitignore'] = [[gitignore]],
['copying.regex'] = [[text]],
['build'] = [[bzl]],
['nvimrc'] = [[vim]],
['.dir_colors'] = [[dircolors]],
['.bashrc'] = [[sh]],
['kbuild'] = [[make]],
['.profile'] = [[sh]],
['.php_cs.dist'] = [[php]],
['gradlew'] = [[sh]],
['settings.stylecop'] = [[xml]],
['makefile.inc'] = [[make]],
['mkfile'] = [[make]],
['.bzrignore'] = [[gitignore]],
['ack'] = [[perl]],
['license.mysql'] = [[text]],
['makefile'] = [[make]],
['vagrantfile'] = [[ruby]],
['nanorc'] = [[nanorc]],
['.bash_profile'] = [[sh]],
['bash_logout'] = [[sh]],
['bash_profile'] = [[sh]],
['click.me'] = [[text]],
['login'] = [[sh]],
['_vimrc'] = [[vim]],
['go.sum'] = [[text]],
['apache2.conf'] = [[apache]],
['use.mask'] = [[text]],
['build.xml'] = [[ant]],
['mmt'] = [[groff]],
['zshenv'] = [[sh]],
['copying'] = [[text]],
['install'] = [[text]],
['test.me'] = [[text]],
['package.mask'] = [[text]],
['.clang-tidy'] = [[yaml]],
['readme.nss'] = [[text]],
['rebar.config.lock'] = [[erlang]],
},
}

View File

@ -0,0 +1,55 @@
local shebang_prefixes = { '/usr/bin/', '/bin/', '/usr/bin/env ', '/bin/env ' }
local shebang_fts = {
['fish'] = 'fish',
['perl'] = 'perl',
['python'] = 'python',
['python2'] = 'python',
['python3'] = 'python',
['bash'] = 'sh',
['sh'] = 'sh',
['zsh'] = 'zsh',
}
local shebang = {}
for _, prefix in ipairs(shebang_prefixes) do
for k, v in pairs(shebang_fts) do
shebang[prefix .. k] = v
end
end
return {
extension = {
['_coffee'] = 'coffee',
['coffee'] = 'coffee',
['cljd'] = 'clojure',
['dart'] = 'dart',
['ex'] = 'elixir',
['exs'] = 'elixir',
['erb'] = 'eruby',
['fnl'] = 'fennel',
['gql'] = 'graphql',
['graphql'] = 'graphql',
['gradle'] = 'groovy',
['hbs'] = 'handlebars',
['hdbs'] = 'handlebars',
['janet'] = 'janet',
['jsx'] = 'javascriptreact',
['jl'] = 'julia',
['kt'] = 'kotlin',
['nix'] = 'nix',
['purs'] = 'purescript',
['rkt'] = 'racket',
['res'] = 'rescript',
['resi'] = 'rescript',
['tsx'] = 'typescriptreact',
['plist'] = 'xml',
},
file_name = {
['cakefile'] = 'coffee',
['.babelrc'] = 'json',
['.eslintrc'] = 'json',
['.firebaserc'] = 'json',
['.prettierrc'] = 'json',
},
shebang = shebang
}

View File

@ -0,0 +1,70 @@
local assert = require('luassert.assert')
local say = require('say')
-- Example usage:
-- local arr = { "one", "two", "three" }
--
-- assert.array(arr).has.no.holes() -- checks the array to not contain holes --> passes
-- assert.array(arr).has.no.holes(4) -- sets explicit length to 4 --> fails
--
-- local first_hole = assert.array(arr).has.holes(4) -- check array of size 4 to contain holes --> passes
-- assert.equal(4, first_hole) -- passes, as the index of the first hole is returned
-- Unique key to store the object we operate on in the state object
-- key must be unique, to make sure we do not have name collissions in the shared state object
local ARRAY_STATE_KEY = "__array_state"
-- The modifier, to store the object in our state
local function array(state, args, level)
assert(args.n > 0, "No array provided to the array-modifier")
assert(rawget(state, ARRAY_STATE_KEY) == nil, "Array already set")
rawset(state, ARRAY_STATE_KEY, args[1])
return state
end
-- The actual assertion that operates on our object, stored via the modifier
local function holes(state, args, level)
local length = args[1]
local arr = rawget(state, ARRAY_STATE_KEY) -- retrieve previously set object
-- only check against nil, metatable types are allowed
assert(arr ~= nil, "No array set, please use the array modifier to set the array to validate")
if length == nil then
length = 0
for i in pairs(arr) do
if type(i) == "number" and
i > length and
math.floor(i) == i then
length = i
end
end
end
assert(type(length) == "number", "expected array length to be of type 'number', got: "..tostring(length))
-- let's do the actual assertion
local missing
for i = 1, length do
if arr[i] == nil then
missing = i
break
end
end
-- format arguments for output strings;
args[1] = missing
args.n = missing and 1 or 0
return missing ~= nil, { missing } -- assert result + first missing index as return value
end
-- Register the proper assertion messages
say:set("assertion.array_holes.positive", [[
Expected array to have holes, but none was found.
]])
say:set("assertion.array_holes.negative", [[
Expected array to not have holes, hole found at position: %s
]])
-- Register the assertion, and the modifier
assert:register("assertion", "holes", holes,
"assertion.array_holes.positive",
"assertion.array_holes.negative")
assert:register("modifier", "array", array)

View File

@ -0,0 +1,182 @@
local s = require 'say'
local astate = require 'luassert.state'
local util = require 'luassert.util'
local unpack = require 'luassert.compatibility'.unpack
local obj -- the returned module table
local level_mt = {}
-- list of namespaces
local namespace = require 'luassert.namespaces'
local function geterror(assertion_message, failure_message, args)
if util.hastostring(failure_message) then
failure_message = tostring(failure_message)
elseif failure_message ~= nil then
failure_message = astate.format_argument(failure_message)
end
local message = s(assertion_message, obj:format(args))
if message and failure_message then
message = failure_message .. "\n" .. message
end
return message or failure_message
end
local __state_meta = {
__call = function(self, ...)
local keys = util.extract_keys("assertion", self.tokens)
local assertion
for _, key in ipairs(keys) do
assertion = namespace.assertion[key] or assertion
end
if assertion then
for _, key in ipairs(keys) do
if namespace.modifier[key] then
namespace.modifier[key].callback(self)
end
end
local arguments = {...}
arguments.n = select('#', ...) -- add argument count for trailing nils
local val, retargs = assertion.callback(self, arguments, util.errorlevel())
if not val == self.mod then
local message = assertion.positive_message
if not self.mod then
message = assertion.negative_message
end
local err = geterror(message, rawget(self,"failure_message"), arguments)
error(err or "assertion failed!", util.errorlevel())
end
if retargs then
return unpack(retargs)
end
return ...
else
local arguments = {...}
arguments.n = select('#', ...)
self.tokens = {}
for _, key in ipairs(keys) do
if namespace.modifier[key] then
namespace.modifier[key].callback(self, arguments, util.errorlevel())
end
end
end
return self
end,
__index = function(self, key)
for token in key:lower():gmatch('[^_]+') do
table.insert(self.tokens, token)
end
return self
end
}
obj = {
state = function() return setmetatable({mod=true, tokens={}}, __state_meta) end,
-- registers a function in namespace
register = function(self, nspace, name, callback, positive_message, negative_message)
local lowername = name:lower()
if not namespace[nspace] then
namespace[nspace] = {}
end
namespace[nspace][lowername] = {
callback = callback,
name = lowername,
positive_message=positive_message,
negative_message=negative_message
}
end,
-- unregisters a function in a namespace
unregister = function(self, nspace, name)
local lowername = name:lower()
if not namespace[nspace] then
namespace[nspace] = {}
end
namespace[nspace][lowername] = nil
end,
-- registers a formatter
-- a formatter takes a single argument, and converts it to a string, or returns nil if it cannot format the argument
add_formatter = function(self, callback)
astate.add_formatter(callback)
end,
-- unregisters a formatter
remove_formatter = function(self, fmtr)
astate.remove_formatter(fmtr)
end,
format = function(self, args)
-- args.n specifies the number of arguments in case of 'trailing nil' arguments which get lost
local nofmt = args.nofmt or {} -- arguments in this list should not be formatted
local fmtargs = args.fmtargs or {} -- additional arguments to be passed to formatter
for i = 1, (args.n or #args) do -- cannot use pairs because table might have nils
if not nofmt[i] then
local val = args[i]
local valfmt = astate.format_argument(val, nil, fmtargs[i])
if valfmt == nil then valfmt = tostring(val) end -- no formatter found
args[i] = valfmt
end
end
return args
end,
set_parameter = function(self, name, value)
astate.set_parameter(name, value)
end,
get_parameter = function(self, name)
return astate.get_parameter(name)
end,
add_spy = function(self, spy)
astate.add_spy(spy)
end,
snapshot = function(self)
return astate.snapshot()
end,
level = function(self, level)
return setmetatable({
level = level
}, level_mt)
end,
-- returns the level if a level-value, otherwise nil
get_level = function(self, level)
if getmetatable(level) ~= level_mt then
return nil -- not a valid error-level
end
return level.level
end,
}
local __meta = {
__call = function(self, bool, message, level, ...)
if not bool then
local err_level = (self:get_level(level) or 1) + 1
error(message or "assertion failed!", err_level)
end
return bool , message , level , ...
end,
__index = function(self, key)
return rawget(self, key) or self.state()[key]
end,
}
return setmetatable(obj, __meta)

View File

@ -0,0 +1,328 @@
-- module will not return anything, only register assertions with the main assert engine
-- assertions take 2 parameters;
-- 1) state
-- 2) arguments list. The list has a member 'n' with the argument count to check for trailing nils
-- 3) level The level of the error position relative to the called function
-- returns; boolean; whether assertion passed
local assert = require('luassert.assert')
local astate = require ('luassert.state')
local util = require ('luassert.util')
local s = require('say')
local function format(val)
return astate.format_argument(val) or tostring(val)
end
local function set_failure_message(state, message)
if message ~= nil then
state.failure_message = message
end
end
local function unique(state, arguments, level)
local list = arguments[1]
local deep
local argcnt = arguments.n
if type(arguments[2]) == "boolean" or (arguments[2] == nil and argcnt > 2) then
deep = arguments[2]
set_failure_message(state, arguments[3])
else
if type(arguments[3]) == "boolean" then
deep = arguments[3]
end
set_failure_message(state, arguments[2])
end
for k,v in pairs(list) do
for k2, v2 in pairs(list) do
if k ~= k2 then
if deep and util.deepcompare(v, v2, true) then
return false
else
if v == v2 then
return false
end
end
end
end
end
return true
end
local function near(state, arguments, level)
local level = (level or 1) + 1
local argcnt = arguments.n
assert(argcnt > 2, s("assertion.internal.argtolittle", { "near", 3, tostring(argcnt) }), level)
local expected = tonumber(arguments[1])
local actual = tonumber(arguments[2])
local tolerance = tonumber(arguments[3])
local numbertype = "number or object convertible to a number"
assert(expected, s("assertion.internal.badargtype", { 1, "near", numbertype, format(arguments[1]) }), level)
assert(actual, s("assertion.internal.badargtype", { 2, "near", numbertype, format(arguments[2]) }), level)
assert(tolerance, s("assertion.internal.badargtype", { 3, "near", numbertype, format(arguments[3]) }), level)
-- switch arguments for proper output message
util.tinsert(arguments, 1, util.tremove(arguments, 2))
arguments[3] = tolerance
arguments.nofmt = arguments.nofmt or {}
arguments.nofmt[3] = true
set_failure_message(state, arguments[4])
return (actual >= expected - tolerance and actual <= expected + tolerance)
end
local function matches(state, arguments, level)
local level = (level or 1) + 1
local argcnt = arguments.n
assert(argcnt > 1, s("assertion.internal.argtolittle", { "matches", 2, tostring(argcnt) }), level)
local pattern = arguments[1]
local actual = nil
if util.hastostring(arguments[2]) or type(arguments[2]) == "number" then
actual = tostring(arguments[2])
end
local err_message
local init_arg_num = 3
for i=3,argcnt,1 do
if arguments[i] and type(arguments[i]) ~= "boolean" and not tonumber(arguments[i]) then
if i == 3 then init_arg_num = init_arg_num + 1 end
err_message = util.tremove(arguments, i)
break
end
end
local init = arguments[3]
local plain = arguments[4]
local stringtype = "string or object convertible to a string"
assert(type(pattern) == "string", s("assertion.internal.badargtype", { 1, "matches", "string", type(arguments[1]) }), level)
assert(actual, s("assertion.internal.badargtype", { 2, "matches", stringtype, format(arguments[2]) }), level)
assert(init == nil or tonumber(init), s("assertion.internal.badargtype", { init_arg_num, "matches", "number", type(arguments[3]) }), level)
-- switch arguments for proper output message
util.tinsert(arguments, 1, util.tremove(arguments, 2))
set_failure_message(state, err_message)
local retargs
local ok
if plain then
ok = (actual:find(pattern, init, plain) ~= nil)
retargs = ok and { pattern } or {}
else
retargs = { actual:match(pattern, init) }
ok = (retargs[1] ~= nil)
end
return ok, retargs
end
local function equals(state, arguments, level)
local level = (level or 1) + 1
local argcnt = arguments.n
assert(argcnt > 1, s("assertion.internal.argtolittle", { "equals", 2, tostring(argcnt) }), level)
local result = arguments[1] == arguments[2]
-- switch arguments for proper output message
util.tinsert(arguments, 1, util.tremove(arguments, 2))
set_failure_message(state, arguments[3])
return result
end
local function same(state, arguments, level)
local level = (level or 1) + 1
local argcnt = arguments.n
assert(argcnt > 1, s("assertion.internal.argtolittle", { "same", 2, tostring(argcnt) }), level)
if type(arguments[1]) == 'table' and type(arguments[2]) == 'table' then
local result, crumbs = util.deepcompare(arguments[1], arguments[2], true)
-- switch arguments for proper output message
util.tinsert(arguments, 1, util.tremove(arguments, 2))
arguments.fmtargs = arguments.fmtargs or {}
arguments.fmtargs[1] = { crumbs = crumbs }
arguments.fmtargs[2] = { crumbs = crumbs }
set_failure_message(state, arguments[3])
return result
end
local result = arguments[1] == arguments[2]
-- switch arguments for proper output message
util.tinsert(arguments, 1, util.tremove(arguments, 2))
set_failure_message(state, arguments[3])
return result
end
local function truthy(state, arguments, level)
set_failure_message(state, arguments[2])
return arguments[1] ~= false and arguments[1] ~= nil
end
local function falsy(state, arguments, level)
return not truthy(state, arguments, level)
end
local function has_error(state, arguments, level)
local level = (level or 1) + 1
local retargs = util.shallowcopy(arguments)
local func = arguments[1]
local err_expected = arguments[2]
local failure_message = arguments[3]
assert(util.callable(func), s("assertion.internal.badargtype", { 1, "error", "function or callable object", type(func) }), level)
local ok, err_actual = pcall(func)
if type(err_actual) == 'string' then
-- remove 'path/to/file:line: ' from string
err_actual = err_actual:gsub('^.-:%d+: ', '', 1)
end
retargs[1] = err_actual
arguments.nofmt = {}
arguments.n = 2
arguments[1] = (ok and '(no error)' or err_actual)
arguments[2] = (err_expected == nil and '(error)' or err_expected)
arguments.nofmt[1] = ok
arguments.nofmt[2] = (err_expected == nil)
set_failure_message(state, failure_message)
if ok or err_expected == nil then
return not ok, retargs
end
if type(err_expected) == 'string' then
-- err_actual must be (convertible to) a string
if util.hastostring(err_actual) then
err_actual = tostring(err_actual)
retargs[1] = err_actual
end
if type(err_actual) == 'string' then
return err_expected == err_actual, retargs
end
elseif type(err_expected) == 'number' then
if type(err_actual) == 'string' then
return tostring(err_expected) == tostring(tonumber(err_actual)), retargs
end
end
return same(state, {err_expected, err_actual, ["n"] = 2}), retargs
end
local function error_matches(state, arguments, level)
local level = (level or 1) + 1
local retargs = util.shallowcopy(arguments)
local argcnt = arguments.n
local func = arguments[1]
local pattern = arguments[2]
assert(argcnt > 1, s("assertion.internal.argtolittle", { "error_matches", 2, tostring(argcnt) }), level)
assert(util.callable(func), s("assertion.internal.badargtype", { 1, "error_matches", "function or callable object", type(func) }), level)
assert(pattern == nil or type(pattern) == "string", s("assertion.internal.badargtype", { 2, "error", "string", type(pattern) }), level)
local failure_message
local init_arg_num = 3
for i=3,argcnt,1 do
if arguments[i] and type(arguments[i]) ~= "boolean" and not tonumber(arguments[i]) then
if i == 3 then init_arg_num = init_arg_num + 1 end
failure_message = util.tremove(arguments, i)
break
end
end
local init = arguments[3]
local plain = arguments[4]
assert(init == nil or tonumber(init), s("assertion.internal.badargtype", { init_arg_num, "matches", "number", type(arguments[3]) }), level)
local ok, err_actual = pcall(func)
if type(err_actual) == 'string' then
-- remove 'path/to/file:line: ' from string
err_actual = err_actual:gsub('^.-:%d+: ', '', 1)
end
retargs[1] = err_actual
arguments.nofmt = {}
arguments.n = 2
arguments[1] = (ok and '(no error)' or err_actual)
arguments[2] = pattern
arguments.nofmt[1] = ok
arguments.nofmt[2] = false
set_failure_message(state, failure_message)
if ok then return not ok, retargs end
if err_actual == nil and pattern == nil then
return true, {}
end
-- err_actual must be (convertible to) a string
if util.hastostring(err_actual) then
err_actual = tostring(err_actual)
retargs[1] = err_actual
end
if type(err_actual) == 'string' then
local ok
local retargs_ok
if plain then
retargs_ok = { pattern }
ok = (err_actual:find(pattern, init, plain) ~= nil)
else
retargs_ok = { err_actual:match(pattern, init) }
ok = (retargs_ok[1] ~= nil)
end
if ok then retargs = retargs_ok end
return ok, retargs
end
return false, retargs
end
local function is_true(state, arguments, level)
util.tinsert(arguments, 2, true)
set_failure_message(state, arguments[3])
return arguments[1] == arguments[2]
end
local function is_false(state, arguments, level)
util.tinsert(arguments, 2, false)
set_failure_message(state, arguments[3])
return arguments[1] == arguments[2]
end
local function is_type(state, arguments, level, etype)
util.tinsert(arguments, 2, "type " .. etype)
arguments.nofmt = arguments.nofmt or {}
arguments.nofmt[2] = true
set_failure_message(state, arguments[3])
return arguments.n > 1 and type(arguments[1]) == etype
end
local function returned_arguments(state, arguments, level)
arguments[1] = tostring(arguments[1])
arguments[2] = tostring(arguments.n - 1)
arguments.nofmt = arguments.nofmt or {}
arguments.nofmt[1] = true
arguments.nofmt[2] = true
if arguments.n < 2 then arguments.n = 2 end
return arguments[1] == arguments[2]
end
local function set_message(state, arguments, level)
state.failure_message = arguments[1]
end
local function is_boolean(state, arguments, level) return is_type(state, arguments, level, "boolean") end
local function is_number(state, arguments, level) return is_type(state, arguments, level, "number") end
local function is_string(state, arguments, level) return is_type(state, arguments, level, "string") end
local function is_table(state, arguments, level) return is_type(state, arguments, level, "table") end
local function is_nil(state, arguments, level) return is_type(state, arguments, level, "nil") end
local function is_userdata(state, arguments, level) return is_type(state, arguments, level, "userdata") end
local function is_function(state, arguments, level) return is_type(state, arguments, level, "function") end
local function is_thread(state, arguments, level) return is_type(state, arguments, level, "thread") end
assert:register("modifier", "message", set_message)
assert:register("assertion", "true", is_true, "assertion.same.positive", "assertion.same.negative")
assert:register("assertion", "false", is_false, "assertion.same.positive", "assertion.same.negative")
assert:register("assertion", "boolean", is_boolean, "assertion.same.positive", "assertion.same.negative")
assert:register("assertion", "number", is_number, "assertion.same.positive", "assertion.same.negative")
assert:register("assertion", "string", is_string, "assertion.same.positive", "assertion.same.negative")
assert:register("assertion", "table", is_table, "assertion.same.positive", "assertion.same.negative")
assert:register("assertion", "nil", is_nil, "assertion.same.positive", "assertion.same.negative")
assert:register("assertion", "userdata", is_userdata, "assertion.same.positive", "assertion.same.negative")
assert:register("assertion", "function", is_function, "assertion.same.positive", "assertion.same.negative")
assert:register("assertion", "thread", is_thread, "assertion.same.positive", "assertion.same.negative")
assert:register("assertion", "returned_arguments", returned_arguments, "assertion.returned_arguments.positive", "assertion.returned_arguments.negative")
assert:register("assertion", "same", same, "assertion.same.positive", "assertion.same.negative")
assert:register("assertion", "matches", matches, "assertion.matches.positive", "assertion.matches.negative")
assert:register("assertion", "match", matches, "assertion.matches.positive", "assertion.matches.negative")
assert:register("assertion", "near", near, "assertion.near.positive", "assertion.near.negative")
assert:register("assertion", "equals", equals, "assertion.equals.positive", "assertion.equals.negative")
assert:register("assertion", "equal", equals, "assertion.equals.positive", "assertion.equals.negative")
assert:register("assertion", "unique", unique, "assertion.unique.positive", "assertion.unique.negative")
assert:register("assertion", "error", has_error, "assertion.error.positive", "assertion.error.negative")
assert:register("assertion", "errors", has_error, "assertion.error.positive", "assertion.error.negative")
assert:register("assertion", "error_matches", error_matches, "assertion.error.positive", "assertion.error.negative")
assert:register("assertion", "error_match", error_matches, "assertion.error.positive", "assertion.error.negative")
assert:register("assertion", "matches_error", error_matches, "assertion.error.positive", "assertion.error.negative")
assert:register("assertion", "match_error", error_matches, "assertion.error.positive", "assertion.error.negative")
assert:register("assertion", "truthy", truthy, "assertion.truthy.positive", "assertion.truthy.negative")
assert:register("assertion", "falsy", falsy, "assertion.falsy.positive", "assertion.falsy.negative")

View File

@ -0,0 +1,3 @@
return {
unpack = table.unpack or unpack,
}

View File

@ -0,0 +1,28 @@
local format = function (str)
if type(str) ~= "string" then return nil end
local result = "Binary string length; " .. tostring(#str) .. " bytes\n"
local i = 1
local hex = ""
local chr = ""
while i <= #str do
local byte = str:byte(i)
hex = string.format("%s%2x ", hex, byte)
if byte < 32 then byte = string.byte(".") end
chr = chr .. string.char(byte)
if math.floor(i/16) == i/16 or i == #str then
-- reached end of line
hex = hex .. string.rep(" ", 16 * 3 - #hex)
chr = chr .. string.rep(" ", 16 - #chr)
result = result .. hex:sub(1, 8 * 3) .. " " .. hex:sub(8*3+1, -1) .. " " .. chr:sub(1,8) .. " " .. chr:sub(9,-1) .. "\n"
hex = ""
chr = ""
end
i = i + 1
end
return result
end
return format

View File

@ -0,0 +1,216 @@
-- module will not return anything, only register formatters with the main assert engine
local assert = require('luassert.assert')
local colors = setmetatable({
none = function(c) return c end
},{ __index = function(self, key)
local ok, term = pcall(require, 'term')
local isatty = io.type(io.stdout) == 'file' and ok and term.isatty(io.stdout)
if not ok or not isatty or not term.colors then
return function(c) return c end
end
return function(c)
for token in key:gmatch("[^%.]+") do
c = term.colors[token](c)
end
return c
end
end
})
local function fmt_string(arg)
if type(arg) == "string" then
return string.format("(string) '%s'", arg)
end
end
-- A version of tostring which formats numbers more precisely.
local function tostr(arg)
if type(arg) ~= "number" then
return tostring(arg)
end
if arg ~= arg then
return "NaN"
elseif arg == 1/0 then
return "Inf"
elseif arg == -1/0 then
return "-Inf"
end
local str = string.format("%.20g", arg)
if math.type and math.type(arg) == "float" and not str:find("[%.,]") then
-- Number is a float but looks like an integer.
-- Insert ".0" after first run of digits.
str = str:gsub("%d+", "%0.0", 1)
end
return str
end
local function fmt_number(arg)
if type(arg) == "number" then
return string.format("(number) %s", tostr(arg))
end
end
local function fmt_boolean(arg)
if type(arg) == "boolean" then
return string.format("(boolean) %s", tostring(arg))
end
end
local function fmt_nil(arg)
if type(arg) == "nil" then
return "(nil)"
end
end
local type_priorities = {
number = 1,
boolean = 2,
string = 3,
table = 4,
["function"] = 5,
userdata = 6,
thread = 7
}
local function is_in_array_part(key, length)
return type(key) == "number" and 1 <= key and key <= length and math.floor(key) == key
end
local function get_sorted_keys(t)
local keys = {}
local nkeys = 0
for key in pairs(t) do
nkeys = nkeys + 1
keys[nkeys] = key
end
local length = #t
local function key_comparator(key1, key2)
local type1, type2 = type(key1), type(key2)
local priority1 = is_in_array_part(key1, length) and 0 or type_priorities[type1] or 8
local priority2 = is_in_array_part(key2, length) and 0 or type_priorities[type2] or 8
if priority1 == priority2 then
if type1 == "string" or type1 == "number" then
return key1 < key2
elseif type1 == "boolean" then
return key1 -- put true before false
end
else
return priority1 < priority2
end
end
table.sort(keys, key_comparator)
return keys, nkeys
end
local function fmt_table(arg, fmtargs)
if type(arg) ~= "table" then
return
end
local tmax = assert:get_parameter("TableFormatLevel")
local showrec = assert:get_parameter("TableFormatShowRecursion")
local errchar = assert:get_parameter("TableErrorHighlightCharacter") or ""
local errcolor = assert:get_parameter("TableErrorHighlightColor") or "none"
local crumbs = fmtargs and fmtargs.crumbs or {}
local cache = {}
local type_desc
if getmetatable(arg) == nil then
type_desc = "(" .. tostring(arg) .. ") "
elseif not pcall(setmetatable, arg, getmetatable(arg)) then
-- cannot set same metatable, so it is protected, skip id
type_desc = "(table) "
else
-- unprotected metatable, temporary remove the mt
local mt = getmetatable(arg)
setmetatable(arg, nil)
type_desc = "(" .. tostring(arg) .. ") "
setmetatable(arg, mt)
end
local function ft(t, l, with_crumbs)
if showrec and cache[t] and cache[t] > 0 then
return "{ ... recursive }"
end
if next(t) == nil then
return "{ }"
end
if l > tmax and tmax >= 0 then
return "{ ... more }"
end
local result = "{"
local keys, nkeys = get_sorted_keys(t)
cache[t] = (cache[t] or 0) + 1
local crumb = crumbs[#crumbs - l + 1]
for i = 1, nkeys do
local k = keys[i]
local v = t[k]
local use_crumbs = with_crumbs and k == crumb
if type(v) == "table" then
v = ft(v, l + 1, use_crumbs)
elseif type(v) == "string" then
v = "'"..v.."'"
end
local ch = use_crumbs and errchar or ""
local indent = string.rep(" ",l * 2 - ch:len())
local mark = (ch:len() == 0 and "" or colors[errcolor](ch))
result = result .. string.format("\n%s%s[%s] = %s", indent, mark, tostr(k), tostr(v))
end
cache[t] = cache[t] - 1
return result .. " }"
end
return type_desc .. ft(arg, 1, true)
end
local function fmt_function(arg)
if type(arg) == "function" then
local debug_info = debug.getinfo(arg)
return string.format("%s @ line %s in %s", tostring(arg), tostring(debug_info.linedefined), tostring(debug_info.source))
end
end
local function fmt_userdata(arg)
if type(arg) == "userdata" then
return string.format("(userdata) '%s'", tostring(arg))
end
end
local function fmt_thread(arg)
if type(arg) == "thread" then
return string.format("(thread) '%s'", tostring(arg))
end
end
assert:add_formatter(fmt_string)
assert:add_formatter(fmt_number)
assert:add_formatter(fmt_boolean)
assert:add_formatter(fmt_nil)
assert:add_formatter(fmt_table)
assert:add_formatter(fmt_function)
assert:add_formatter(fmt_userdata)
assert:add_formatter(fmt_thread)
-- Set default table display depth for table formatter
assert:set_parameter("TableFormatLevel", 3)
assert:set_parameter("TableFormatShowRecursion", false)
assert:set_parameter("TableErrorHighlightCharacter", "*")
assert:set_parameter("TableErrorHighlightColor", "none")

View File

@ -0,0 +1,17 @@
local assert = require('luassert.assert')
assert._COPYRIGHT = "Copyright (c) 2018 Olivine Labs, LLC."
assert._DESCRIPTION = "Extends Lua's built-in assertions to provide additional tests and the ability to create your own."
assert._VERSION = "Luassert 1.8.0"
-- load basic asserts
require('luassert.assertions')
require('luassert.modifiers')
require('luassert.array')
require('luassert.matchers')
require('luassert.formatters')
-- load default language
require('luassert.languages.en')
return assert

View File

@ -0,0 +1,48 @@
local s = require('say')
s:set_namespace('en')
s:set("assertion.same.positive", "Expected objects to be the same.\nPassed in:\n%s\nExpected:\n%s")
s:set("assertion.same.negative", "Expected objects to not be the same.\nPassed in:\n%s\nDid not expect:\n%s")
s:set("assertion.equals.positive", "Expected objects to be equal.\nPassed in:\n%s\nExpected:\n%s")
s:set("assertion.equals.negative", "Expected objects to not be equal.\nPassed in:\n%s\nDid not expect:\n%s")
s:set("assertion.near.positive", "Expected values to be near.\nPassed in:\n%s\nExpected:\n%s +/- %s")
s:set("assertion.near.negative", "Expected values to not be near.\nPassed in:\n%s\nDid not expect:\n%s +/- %s")
s:set("assertion.matches.positive", "Expected strings to match.\nPassed in:\n%s\nExpected:\n%s")
s:set("assertion.matches.negative", "Expected strings not to match.\nPassed in:\n%s\nDid not expect:\n%s")
s:set("assertion.unique.positive", "Expected object to be unique:\n%s")
s:set("assertion.unique.negative", "Expected object to not be unique:\n%s")
s:set("assertion.error.positive", "Expected a different error.\nCaught:\n%s\nExpected:\n%s")
s:set("assertion.error.negative", "Expected no error, but caught:\n%s")
s:set("assertion.truthy.positive", "Expected to be truthy, but value was:\n%s")
s:set("assertion.truthy.negative", "Expected to not be truthy, but value was:\n%s")
s:set("assertion.falsy.positive", "Expected to be falsy, but value was:\n%s")
s:set("assertion.falsy.negative", "Expected to not be falsy, but value was:\n%s")
s:set("assertion.called.positive", "Expected to be called %s time(s), but was called %s time(s)")
s:set("assertion.called.negative", "Expected not to be called exactly %s time(s), but it was.")
s:set("assertion.called_at_least.positive", "Expected to be called at least %s time(s), but was called %s time(s)")
s:set("assertion.called_at_most.positive", "Expected to be called at most %s time(s), but was called %s time(s)")
s:set("assertion.called_more_than.positive", "Expected to be called more than %s time(s), but was called %s time(s)")
s:set("assertion.called_less_than.positive", "Expected to be called less than %s time(s), but was called %s time(s)")
s:set("assertion.called_with.positive", "Function was not called with the arguments")
s:set("assertion.called_with.negative", "Function was called with the arguments")
s:set("assertion.returned_with.positive", "Function was not returned with the arguments")
s:set("assertion.returned_with.negative", "Function was returned with the arguments")
s:set("assertion.returned_arguments.positive", "Expected to be called with %s argument(s), but was called with %s")
s:set("assertion.returned_arguments.negative", "Expected not to be called with %s argument(s), but was called with %s")
-- errors
s:set("assertion.internal.argtolittle", "the '%s' function requires a minimum of %s arguments, got: %s")
s:set("assertion.internal.badargtype", "bad argument #%s to '%s' (%s expected, got %s)")

View File

@ -0,0 +1,80 @@
local namespace = require 'luassert.namespaces'
local util = require 'luassert.util'
local matcher_mt = {
__call = function(self, value)
return self.callback(value) == self.mod
end,
}
local state_mt = {
__call = function(self, ...)
local keys = util.extract_keys("matcher", self.tokens)
self.tokens = {}
local matcher
for _, key in ipairs(keys) do
matcher = namespace.matcher[key] or matcher
end
if matcher then
for _, key in ipairs(keys) do
if namespace.modifier[key] then
namespace.modifier[key].callback(self)
end
end
local arguments = {...}
arguments.n = select('#', ...) -- add argument count for trailing nils
local matches = matcher.callback(self, arguments, util.errorlevel())
return setmetatable({
name = matcher.name,
mod = self.mod,
callback = matches,
}, matcher_mt)
else
local arguments = {...}
arguments.n = select('#', ...) -- add argument count for trailing nils
for _, key in ipairs(keys) do
if namespace.modifier[key] then
namespace.modifier[key].callback(self, arguments, util.errorlevel())
end
end
end
return self
end,
__index = function(self, key)
for token in key:lower():gmatch('[^_]+') do
table.insert(self.tokens, token)
end
return self
end
}
local match = {
_ = setmetatable({mod=true, callback=function() return true end}, matcher_mt),
state = function() return setmetatable({mod=true, tokens={}}, state_mt) end,
is_matcher = function(object)
return type(object) == "table" and getmetatable(object) == matcher_mt
end,
is_ref_matcher = function(object)
local ismatcher = (type(object) == "table" and getmetatable(object) == matcher_mt)
return ismatcher and object.name == "ref"
end,
}
local mt = {
__index = function(self, key)
return rawget(self, key) or self.state()[key]
end,
}
return setmetatable(match, mt)

View File

@ -0,0 +1,61 @@
local assert = require('luassert.assert')
local match = require ('luassert.match')
local s = require('say')
local function none(state, arguments, level)
local level = (level or 1) + 1
local argcnt = arguments.n
assert(argcnt > 0, s("assertion.internal.argtolittle", { "none", 1, tostring(argcnt) }), level)
for i = 1, argcnt do
assert(match.is_matcher(arguments[i]), s("assertion.internal.badargtype", { 1, "none", "matcher", type(arguments[i]) }), level)
end
return function(value)
for _, matcher in ipairs(arguments) do
if matcher(value) then
return false
end
end
return true
end
end
local function any(state, arguments, level)
local level = (level or 1) + 1
local argcnt = arguments.n
assert(argcnt > 0, s("assertion.internal.argtolittle", { "any", 1, tostring(argcnt) }), level)
for i = 1, argcnt do
assert(match.is_matcher(arguments[i]), s("assertion.internal.badargtype", { 1, "any", "matcher", type(arguments[i]) }), level)
end
return function(value)
for _, matcher in ipairs(arguments) do
if matcher(value) then
return true
end
end
return false
end
end
local function all(state, arguments, level)
local level = (level or 1) + 1
local argcnt = arguments.n
assert(argcnt > 0, s("assertion.internal.argtolittle", { "all", 1, tostring(argcnt) }), level)
for i = 1, argcnt do
assert(match.is_matcher(arguments[i]), s("assertion.internal.badargtype", { 1, "all", "matcher", type(arguments[i]) }), level)
end
return function(value)
for _, matcher in ipairs(arguments) do
if not matcher(value) then
return false
end
end
return true
end
end
assert:register("matcher", "none_of", none)
assert:register("matcher", "any_of", any)
assert:register("matcher", "all_of", all)

View File

@ -0,0 +1,174 @@
-- module will return the list of matchers, and registers matchers with the main assert engine
-- matchers take 1 parameters;
-- 1) state
-- 2) arguments list. The list has a member 'n' with the argument count to check for trailing nils
-- 3) level The level of the error position relative to the called function
-- returns; function (or callable object); a function that, given an argument, returns a boolean
local assert = require('luassert.assert')
local astate = require('luassert.state')
local util = require('luassert.util')
local s = require('say')
local function format(val)
return astate.format_argument(val) or tostring(val)
end
local function unique(state, arguments, level)
local deep = arguments[1]
return function(value)
local list = value
for k,v in pairs(list) do
for k2, v2 in pairs(list) do
if k ~= k2 then
if deep and util.deepcompare(v, v2, true) then
return false
else
if v == v2 then
return false
end
end
end
end
end
return true
end
end
local function near(state, arguments, level)
local level = (level or 1) + 1
local argcnt = arguments.n
assert(argcnt > 1, s("assertion.internal.argtolittle", { "near", 2, tostring(argcnt) }), level)
local expected = tonumber(arguments[1])
local tolerance = tonumber(arguments[2])
local numbertype = "number or object convertible to a number"
assert(expected, s("assertion.internal.badargtype", { 1, "near", numbertype, format(arguments[1]) }), level)
assert(tolerance, s("assertion.internal.badargtype", { 2, "near", numbertype, format(arguments[2]) }), level)
return function(value)
local actual = tonumber(value)
if not actual then return false end
return (actual >= expected - tolerance and actual <= expected + tolerance)
end
end
local function matches(state, arguments, level)
local level = (level or 1) + 1
local argcnt = arguments.n
assert(argcnt > 0, s("assertion.internal.argtolittle", { "matches", 1, tostring(argcnt) }), level)
local pattern = arguments[1]
local init = arguments[2]
local plain = arguments[3]
local stringtype = "string or object convertible to a string"
assert(type(pattern) == "string", s("assertion.internal.badargtype", { 1, "matches", "string", type(arguments[1]) }), level)
assert(init == nil or tonumber(init), s("assertion.internal.badargtype", { 2, "matches", "number", type(arguments[2]) }), level)
return function(value)
local actualtype = type(value)
local actual = nil
if actualtype == "string" or actualtype == "number" or
actualtype == "table" and (getmetatable(value) or {}).__tostring then
actual = tostring(value)
end
if not actual then return false end
return (actual:find(pattern, init, plain) ~= nil)
end
end
local function equals(state, arguments, level)
local level = (level or 1) + 1
local argcnt = arguments.n
assert(argcnt > 0, s("assertion.internal.argtolittle", { "equals", 1, tostring(argcnt) }), level)
return function(value)
return value == arguments[1]
end
end
local function same(state, arguments, level)
local level = (level or 1) + 1
local argcnt = arguments.n
assert(argcnt > 0, s("assertion.internal.argtolittle", { "same", 1, tostring(argcnt) }), level)
return function(value)
if type(value) == 'table' and type(arguments[1]) == 'table' then
local result = util.deepcompare(value, arguments[1], true)
return result
end
return value == arguments[1]
end
end
local function ref(state, arguments, level)
local level = (level or 1) + 1
local argcnt = arguments.n
local argtype = type(arguments[1])
local isobject = (argtype == "table" or argtype == "function" or argtype == "thread" or argtype == "userdata")
assert(argcnt > 0, s("assertion.internal.argtolittle", { "ref", 1, tostring(argcnt) }), level)
assert(isobject, s("assertion.internal.badargtype", { 1, "ref", "object", argtype }), level)
return function(value)
return value == arguments[1]
end
end
local function is_true(state, arguments, level)
return function(value)
return value == true
end
end
local function is_false(state, arguments, level)
return function(value)
return value == false
end
end
local function truthy(state, arguments, level)
return function(value)
return value ~= false and value ~= nil
end
end
local function falsy(state, arguments, level)
local is_truthy = truthy(state, arguments, level)
return function(value)
return not is_truthy(value)
end
end
local function is_type(state, arguments, level, etype)
return function(value)
return type(value) == etype
end
end
local function is_nil(state, arguments, level) return is_type(state, arguments, level, "nil") end
local function is_boolean(state, arguments, level) return is_type(state, arguments, level, "boolean") end
local function is_number(state, arguments, level) return is_type(state, arguments, level, "number") end
local function is_string(state, arguments, level) return is_type(state, arguments, level, "string") end
local function is_table(state, arguments, level) return is_type(state, arguments, level, "table") end
local function is_function(state, arguments, level) return is_type(state, arguments, level, "function") end
local function is_userdata(state, arguments, level) return is_type(state, arguments, level, "userdata") end
local function is_thread(state, arguments, level) return is_type(state, arguments, level, "thread") end
assert:register("matcher", "true", is_true)
assert:register("matcher", "false", is_false)
assert:register("matcher", "nil", is_nil)
assert:register("matcher", "boolean", is_boolean)
assert:register("matcher", "number", is_number)
assert:register("matcher", "string", is_string)
assert:register("matcher", "table", is_table)
assert:register("matcher", "function", is_function)
assert:register("matcher", "userdata", is_userdata)
assert:register("matcher", "thread", is_thread)
assert:register("matcher", "ref", ref)
assert:register("matcher", "same", same)
assert:register("matcher", "matches", matches)
assert:register("matcher", "match", matches)
assert:register("matcher", "near", near)
assert:register("matcher", "equals", equals)
assert:register("matcher", "equal", equals)
assert:register("matcher", "unique", unique)
assert:register("matcher", "truthy", truthy)
assert:register("matcher", "falsy", falsy)

View File

@ -0,0 +1,3 @@
-- load basic machers
require('luassert.matchers.core')
require('luassert.matchers.composite')

View File

@ -0,0 +1,61 @@
-- module will return a mock module table, and will not register any assertions
local spy = require 'luassert.spy'
local stub = require 'luassert.stub'
local function mock_apply(object, action)
if type(object) ~= "table" then return end
if spy.is_spy(object) then
return object[action](object)
end
for k,v in pairs(object) do
mock_apply(v, action)
end
return object
end
local mock
mock = {
new = function(object, dostub, func, self, key)
local visited = {}
local function do_mock(object, self, key)
local mock_handlers = {
["table"] = function()
if spy.is_spy(object) or visited[object] then return end
visited[object] = true
for k,v in pairs(object) do
object[k] = do_mock(v, object, k)
end
return object
end,
["function"] = function()
if dostub then
return stub(self, key, func)
elseif self==nil then
return spy.new(object)
else
return spy.on(self, key)
end
end
}
local handler = mock_handlers[type(object)]
return handler and handler() or object
end
return do_mock(object, self, key)
end,
clear = function(object)
return mock_apply(object, "clear")
end,
revert = function(object)
return mock_apply(object, "revert")
end
}
return setmetatable(mock, {
__call = function(self, ...)
-- mock originally was a function only. Now that it is a module table
-- the __call method is required for backward compatibility
return mock.new(...)
end
})

View File

@ -0,0 +1,19 @@
-- module will not return anything, only register assertions/modifiers with the main assert engine
local assert = require('luassert.assert')
local function is(state)
return state
end
local function is_not(state)
state.mod = not state.mod
return state
end
assert:register("modifier", "is", is)
assert:register("modifier", "are", is)
assert:register("modifier", "was", is)
assert:register("modifier", "has", is)
assert:register("modifier", "does", is)
assert:register("modifier", "not", is_not)
assert:register("modifier", "no", is_not)

View File

@ -0,0 +1,2 @@
-- stores the list of namespaces
return {}

View File

@ -0,0 +1,171 @@
-- module will return spy table, and register its assertions with the main assert engine
local assert = require('luassert.assert')
local util = require('luassert.util')
-- Spy metatable
local spy_mt = {
__call = function(self, ...)
local arguments = {...}
arguments.n = select('#',...) -- add argument count for trailing nils
table.insert(self.calls, util.copyargs(arguments))
local function get_returns(...)
local returnvals = {...}
returnvals.n = select('#',...) -- add argument count for trailing nils
table.insert(self.returnvals, util.copyargs(returnvals))
return ...
end
return get_returns(self.callback(...))
end
}
local spy -- must make local before defining table, because table contents refers to the table (recursion)
spy = {
new = function(callback)
callback = callback or function() end
if not util.callable(callback) then
error("Cannot spy on type '" .. type(callback) .. "', only on functions or callable elements", util.errorlevel())
end
local s = setmetatable({
calls = {},
returnvals = {},
callback = callback,
target_table = nil, -- these will be set when using 'spy.on'
target_key = nil,
revert = function(self)
if not self.reverted then
if self.target_table and self.target_key then
self.target_table[self.target_key] = self.callback
end
self.reverted = true
end
return self.callback
end,
clear = function(self)
self.calls = {}
self.returnvals = {}
return self
end,
called = function(self, times, compare)
if times or compare then
local compare = compare or function(count, expected) return count == expected end
return compare(#self.calls, times), #self.calls
end
return (#self.calls > 0), #self.calls
end,
called_with = function(self, args)
return util.matchargs(self.calls, args) ~= nil
end,
returned_with = function(self, args)
return util.matchargs(self.returnvals, args) ~= nil
end
}, spy_mt)
assert:add_spy(s) -- register with the current state
return s
end,
is_spy = function(object)
return type(object) == "table" and getmetatable(object) == spy_mt
end,
on = function(target_table, target_key)
local s = spy.new(target_table[target_key])
target_table[target_key] = s
-- store original data
s.target_table = target_table
s.target_key = target_key
return s
end
}
local function set_spy(state, arguments, level)
state.payload = arguments[1]
if arguments[2] ~= nil then
state.failure_message = arguments[2]
end
end
local function returned_with(state, arguments, level)
local level = (level or 1) + 1
local payload = rawget(state, "payload")
if payload and payload.returned_with then
return state.payload:returned_with(arguments)
else
error("'returned_with' must be chained after 'spy(aspy)'", level)
end
end
local function called_with(state, arguments, level)
local level = (level or 1) + 1
local payload = rawget(state, "payload")
if payload and payload.called_with then
return state.payload:called_with(arguments)
else
error("'called_with' must be chained after 'spy(aspy)'", level)
end
end
local function called(state, arguments, level, compare)
local level = (level or 1) + 1
local num_times = arguments[1]
if not num_times and not state.mod then
state.mod = true
num_times = 0
end
local payload = rawget(state, "payload")
if payload and type(payload) == "table" and payload.called then
local result, count = state.payload:called(num_times, compare)
arguments[1] = tostring(num_times or ">0")
util.tinsert(arguments, 2, tostring(count))
arguments.nofmt = arguments.nofmt or {}
arguments.nofmt[1] = true
arguments.nofmt[2] = true
return result
elseif payload and type(payload) == "function" then
error("When calling 'spy(aspy)', 'aspy' must not be the original function, but the spy function replacing the original", level)
else
error("'called' must be chained after 'spy(aspy)'", level)
end
end
local function called_at_least(state, arguments, level)
local level = (level or 1) + 1
return called(state, arguments, level, function(count, expected) return count >= expected end)
end
local function called_at_most(state, arguments, level)
local level = (level or 1) + 1
return called(state, arguments, level, function(count, expected) return count <= expected end)
end
local function called_more_than(state, arguments, level)
local level = (level or 1) + 1
return called(state, arguments, level, function(count, expected) return count > expected end)
end
local function called_less_than(state, arguments, level)
local level = (level or 1) + 1
return called(state, arguments, level, function(count, expected) return count < expected end)
end
assert:register("modifier", "spy", set_spy)
assert:register("assertion", "returned_with", returned_with, "assertion.returned_with.positive", "assertion.returned_with.negative")
assert:register("assertion", "called_with", called_with, "assertion.called_with.positive", "assertion.called_with.negative")
assert:register("assertion", "called", called, "assertion.called.positive", "assertion.called.negative")
assert:register("assertion", "called_at_least", called_at_least, "assertion.called_at_least.positive", "assertion.called_less_than.positive")
assert:register("assertion", "called_at_most", called_at_most, "assertion.called_at_most.positive", "assertion.called_more_than.positive")
assert:register("assertion", "called_more_than", called_more_than, "assertion.called_more_than.positive", "assertion.called_at_most.positive")
assert:register("assertion", "called_less_than", called_less_than, "assertion.called_less_than.positive", "assertion.called_at_least.positive")
return setmetatable(spy, {
__call = function(self, ...)
return spy.new(...)
end
})

View File

@ -0,0 +1,128 @@
-- maintains a state of the assert engine in a linked-list fashion
-- records; formatters, parameters, spies and stubs
local state_mt = {
__call = function(self)
self:revert()
end
}
local spies_mt = { __mode = "kv" }
local nilvalue = {} -- unique ID to refer to nil values for parameters
-- will hold the current state
local current
-- exported module table
local state = {}
------------------------------------------------------
-- Reverts to a (specific) snapshot.
-- @param self (optional) the snapshot to revert to. If not provided, it will revert to the last snapshot.
state.revert = function(self)
if not self then
-- no snapshot given, so move 1 up
self = current
if not self.previous then
-- top of list, no previous one, nothing to do
return
end
end
if getmetatable(self) ~= state_mt then error("Value provided is not a valid snapshot", 2) end
if self.next then
self.next:revert()
end
-- revert formatters in 'last'
self.formatters = {}
-- revert parameters in 'last'
self.parameters = {}
-- revert spies/stubs in 'last'
for s,_ in pairs(self.spies) do
self.spies[s] = nil
s:revert()
end
setmetatable(self, nil) -- invalidate as a snapshot
current = self.previous
current.next = nil
end
------------------------------------------------------
-- Creates a new snapshot.
-- @return snapshot table
state.snapshot = function()
local s = current
local new = setmetatable ({
formatters = {},
parameters = {},
spies = setmetatable({}, spies_mt),
previous = current,
revert = state.revert,
}, state_mt)
if current then current.next = new end
current = new
return current
end
-- FORMATTERS
state.add_formatter = function(callback)
table.insert(current.formatters, 1, callback)
end
state.remove_formatter = function(callback, s)
s = s or current
for i, v in ipairs(s.formatters) do
if v == callback then
table.remove(s.formatters, i)
break
end
end
-- wasn't found, so traverse up 1 state
if s.previous then
state.remove_formatter(callback, s.previous)
end
end
state.format_argument = function(val, s, fmtargs)
s = s or current
for _, fmt in ipairs(s.formatters) do
local valfmt = fmt(val, fmtargs)
if valfmt ~= nil then return valfmt end
end
-- nothing found, check snapshot 1 up in list
if s.previous then
return state.format_argument(val, s.previous, fmtargs)
end
return nil -- end of list, couldn't format
end
-- PARAMETERS
state.set_parameter = function(name, value)
if value == nil then value = nilvalue end
current.parameters[name] = value
end
state.get_parameter = function(name, s)
s = s or current
local val = s.parameters[name]
if val == nil and s.previous then
-- not found, so check 1 up in list
return state.get_parameter(name, s.previous)
end
if val ~= nilvalue then
return val
end
return nil
end
-- SPIES / STUBS
state.add_spy = function(spy)
current.spies[spy] = true
end
state.snapshot() -- create initial state
return state

View File

@ -0,0 +1,111 @@
-- module will return a stub module table
local assert = require 'luassert.assert'
local spy = require 'luassert.spy'
local util = require 'luassert.util'
local unpack = require 'luassert.compatibility'.unpack
local stub = {}
function stub.new(object, key, ...)
if object == nil and key == nil then
-- called without arguments, create a 'blank' stub
object = {}
key = ""
end
local return_values_count = select("#", ...)
local return_values = {...}
assert(type(object) == "table" and key ~= nil, "stub.new(): Can only create stub on a table key, call with 2 params; table, key", util.errorlevel())
assert(object[key] == nil or util.callable(object[key]), "stub.new(): The element for which to create a stub must either be callable, or be nil", util.errorlevel())
local old_elem = object[key] -- keep existing element (might be nil!)
local fn = (return_values_count == 1 and util.callable(return_values[1]) and return_values[1])
local defaultfunc = fn or function()
return unpack(return_values, 1, return_values_count)
end
local oncalls = {}
local callbacks = {}
local stubfunc = function(...)
local args = {...}
args.n = select('#', ...)
local match = util.matchargs(oncalls, args)
if match then
return callbacks[match](...)
end
return defaultfunc(...)
end
object[key] = stubfunc -- set the stubfunction
local s = spy.on(object, key) -- create a spy on top of the stub function
local spy_revert = s.revert -- keep created revert function
s.revert = function(self) -- wrap revert function to restore original element
if not self.reverted then
spy_revert(self)
object[key] = old_elem
self.reverted = true
end
return old_elem
end
s.returns = function(...)
local return_args = {...}
local n = select('#', ...)
defaultfunc = function()
return unpack(return_args, 1, n)
end
return s
end
s.invokes = function(func)
defaultfunc = function(...)
return func(...)
end
return s
end
s.by_default = {
returns = s.returns,
invokes = s.invokes,
}
s.on_call_with = function(...)
local match_args = {...}
match_args.n = select('#', ...)
match_args = util.copyargs(match_args)
return {
returns = function(...)
local return_args = {...}
local n = select('#', ...)
table.insert(oncalls, match_args)
callbacks[match_args] = function()
return unpack(return_args, 1, n)
end
return s
end,
invokes = function(func)
table.insert(oncalls, match_args)
callbacks[match_args] = function(...)
return func(...)
end
return s
end
}
end
return s
end
local function set_stub(state, arguments)
state.payload = arguments[1]
state.failure_message = arguments[2]
end
assert:register("modifier", "stub", set_stub)
return setmetatable(stub, {
__call = function(self, ...)
-- stub originally was a function only. Now that it is a module table
-- the __call method is required for backward compatibility
return stub.new(...)
end
})

View File

@ -0,0 +1,286 @@
local util = {}
function util.deepcompare(t1,t2,ignore_mt,cycles,thresh1,thresh2)
local ty1 = type(t1)
local ty2 = type(t2)
-- non-table types can be directly compared
if ty1 ~= 'table' or ty2 ~= 'table' then return t1 == t2 end
local mt1 = debug.getmetatable(t1)
local mt2 = debug.getmetatable(t2)
-- would equality be determined by metatable __eq?
if mt1 and mt1 == mt2 and mt1.__eq then
-- then use that unless asked not to
if not ignore_mt then return t1 == t2 end
else -- we can skip the deep comparison below if t1 and t2 share identity
if rawequal(t1, t2) then return true end
end
-- handle recursive tables
cycles = cycles or {{},{}}
thresh1, thresh2 = (thresh1 or 1), (thresh2 or 1)
cycles[1][t1] = (cycles[1][t1] or 0)
cycles[2][t2] = (cycles[2][t2] or 0)
if cycles[1][t1] == 1 or cycles[2][t2] == 1 then
thresh1 = cycles[1][t1] + 1
thresh2 = cycles[2][t2] + 1
end
if cycles[1][t1] > thresh1 and cycles[2][t2] > thresh2 then
return true
end
cycles[1][t1] = cycles[1][t1] + 1
cycles[2][t2] = cycles[2][t2] + 1
for k1,v1 in next, t1 do
local v2 = t2[k1]
if v2 == nil then
return false, {k1}
end
local same, crumbs = util.deepcompare(v1,v2,nil,cycles,thresh1,thresh2)
if not same then
crumbs = crumbs or {}
table.insert(crumbs, k1)
return false, crumbs
end
end
for k2,_ in next, t2 do
-- only check whether each element has a t1 counterpart, actual comparison
-- has been done in first loop above
if t1[k2] == nil then return false, {k2} end
end
cycles[1][t1] = cycles[1][t1] - 1
cycles[2][t2] = cycles[2][t2] - 1
return true
end
function util.shallowcopy(t)
if type(t) ~= "table" then return t end
local copy = {}
for k,v in next, t do
copy[k] = v
end
return copy
end
function util.deepcopy(t, deepmt, cache)
local spy = require 'luassert.spy'
if type(t) ~= "table" then return t end
local copy = {}
-- handle recursive tables
local cache = cache or {}
if cache[t] then return cache[t] end
cache[t] = copy
for k,v in next, t do
copy[k] = (spy.is_spy(v) and v or util.deepcopy(v, deepmt, cache))
end
if deepmt then
debug.setmetatable(copy, util.deepcopy(debug.getmetatable(t, nil, cache)))
else
debug.setmetatable(copy, debug.getmetatable(t))
end
return copy
end
-----------------------------------------------
-- Copies arguments as a list of arguments
-- @param args the arguments of which to copy
-- @return the copy of the arguments
function util.copyargs(args)
local copy = {}
local match = require 'luassert.match'
local spy = require 'luassert.spy'
for k,v in pairs(args) do
copy[k] = ((match.is_matcher(v) or spy.is_spy(v)) and v or util.deepcopy(v))
end
return { vals = copy, refs = util.shallowcopy(args) }
end
-----------------------------------------------
-- Finds matching arguments in a saved list of arguments
-- @param argslist list of arguments from which to search
-- @param args the arguments of which to find a match
-- @return the matching arguments if a match is found, otherwise nil
function util.matchargs(argslist, args)
local function matches(t1, t2, t1refs)
local match = require 'luassert.match'
for k1,v1 in pairs(t1) do
local v2 = t2[k1]
if match.is_matcher(v1) then
if not v1(v2) then return false end
elseif match.is_matcher(v2) then
if match.is_ref_matcher(v2) then v1 = t1refs[k1] end
if not v2(v1) then return false end
elseif (v2 == nil or not util.deepcompare(v1,v2)) then
return false
end
end
for k2,v2 in pairs(t2) do
-- only check wether each element has a t1 counterpart, actual comparison
-- has been done in first loop above
local v1 = t1[k2]
if v1 == nil then
-- no t1 counterpart, so try to compare using matcher
if match.is_matcher(v2) then
if not v2(v1) then return false end
else
return false
end
end
end
return true
end
for k,v in ipairs(argslist) do
if matches(v.vals, args, v.refs) then
return v
end
end
return nil
end
-----------------------------------------------
-- table.insert() replacement that respects nil values.
-- The function will use table field 'n' as indicator of the
-- table length, if not set, it will be added.
-- @param t table into which to insert
-- @param pos (optional) position in table where to insert. NOTE: not optional if you want to insert a nil-value!
-- @param val value to insert
-- @return No return values
function util.tinsert(...)
-- check optional POS value
local args = {...}
local c = select('#',...)
local t = args[1]
local pos = args[2]
local val = args[3]
if c < 3 then
val = pos
pos = nil
end
-- set length indicator n if not present (+1)
t.n = (t.n or #t) + 1
if not pos then
pos = t.n
elseif pos > t.n then
-- out of our range
t[pos] = val
t.n = pos
end
-- shift everything up 1 pos
for i = t.n, pos + 1, -1 do
t[i]=t[i-1]
end
-- add element to be inserted
t[pos] = val
end
-----------------------------------------------
-- table.remove() replacement that respects nil values.
-- The function will use table field 'n' as indicator of the
-- table length, if not set, it will be added.
-- @param t table from which to remove
-- @param pos (optional) position in table to remove
-- @return No return values
function util.tremove(t, pos)
-- set length indicator n if not present (+1)
t.n = t.n or #t
if not pos then
pos = t.n
elseif pos > t.n then
local removed = t[pos]
-- out of our range
t[pos] = nil
return removed
end
local removed = t[pos]
-- shift everything up 1 pos
for i = pos, t.n do
t[i]=t[i+1]
end
-- set size, clean last
t[t.n] = nil
t.n = t.n - 1
return removed
end
-----------------------------------------------
-- Checks an element to be callable.
-- The type must either be a function or have a metatable
-- containing an '__call' function.
-- @param object element to inspect on being callable or not
-- @return boolean, true if the object is callable
function util.callable(object)
return type(object) == "function" or type((debug.getmetatable(object) or {}).__call) == "function"
end
-----------------------------------------------
-- Checks an element has tostring.
-- The type must either be a string or have a metatable
-- containing an '__tostring' function.
-- @param object element to inspect on having tostring or not
-- @return boolean, true if the object has tostring
function util.hastostring(object)
return type(object) == "string" or type((debug.getmetatable(object) or {}).__tostring) == "function"
end
-----------------------------------------------
-- Find the first level, not defined in the same file as the caller's
-- code file to properly report an error.
-- @param level the level to use as the caller's source file
-- @return number, the level of which to report an error
function util.errorlevel(level)
local level = (level or 1) + 1 -- add one to get level of the caller
local info = debug.getinfo(level)
local source = (info or {}).source
local file = source
while file and (file == source or source == "=(tail call)") do
level = level + 1
info = debug.getinfo(level)
source = (info or {}).source
end
if level > 1 then level = level - 1 end -- deduct call to errorlevel() itself
return level
end
-----------------------------------------------
-- Extract modifier and namespace keys from list of tokens.
-- @param nspace the namespace from which to match tokens
-- @param tokens list of tokens to search for keys
-- @return table, list of keys that were extracted
function util.extract_keys(nspace, tokens)
local namespace = require 'luassert.namespaces'
-- find valid keys by coalescing tokens as needed, starting from the end
local keys = {}
local key = nil
local i = #tokens
while i > 0 do
local token = tokens[i]
key = key and (token .. '_' .. key) or token
-- find longest matching key in the given namespace
local longkey = i > 1 and (tokens[i-1] .. '_' .. key) or nil
while i > 1 and longkey and namespace[nspace][longkey] do
key = longkey
i = i - 1
token = tokens[i]
longkey = (token .. '_' .. key)
end
if namespace.modifier[key] or namespace[nspace][key] then
table.insert(keys, 1, key)
key = nil
end
i = i - 1
end
-- if there's anything left we didn't recognize it
if key then
error("luassert: unknown modifier/" .. nspace .. ": '" .. key .."'", util.errorlevel(2))
end
return keys
end
return util

View File

@ -0,0 +1,14 @@
local util = require "plenary.async.util"
return setmetatable({}, {
__index = function(t, k)
return function(...)
-- if we are in a fast event await the scheduler
if vim.in_fast_event() then
util.scheduler()
end
vim.api[k](...)
end
end,
})

View File

@ -0,0 +1,117 @@
local co = coroutine
local vararg = require "plenary.vararg"
local errors = require "plenary.errors"
local traceback_error = errors.traceback_error
local f = require "plenary.functional"
local M = {}
---because we can't store varargs
local function callback_or_next(step, thread, callback, ...)
local stat = f.first(...)
if not stat then
error(string.format("The coroutine failed with this message: %s", f.second(...)))
end
if co.status(thread) == "dead" then
if callback == nil then
return
end
callback(select(2, ...))
else
local returned_function = f.second(...)
local nargs = f.third(...)
assert(type(returned_function) == "function", "type error :: expected func")
returned_function(vararg.rotate(nargs, step, select(4, ...)))
end
end
---Executes a future with a callback when it is done
---@param async_function Future: the future to execute
---@param callback function: the callback to call when done
local execute = function(async_function, callback, ...)
assert(type(async_function) == "function", "type error :: expected func")
local thread = co.create(async_function)
local step
step = function(...)
callback_or_next(step, thread, callback, co.resume(thread, ...))
end
step(...)
end
local add_leaf_function
do
---A table to store all leaf async functions
_PlenaryLeafTable = setmetatable({}, {
__mode = "k",
})
add_leaf_function = function(async_func, argc)
assert(_PlenaryLeafTable[async_func] == nil, "Async function should not already be in the table")
_PlenaryLeafTable[async_func] = argc
end
function M.is_leaf_function(async_func)
return _PlenaryLeafTable[async_func] ~= nil
end
function M.get_leaf_function_argc(async_func)
return _PlenaryLeafTable[async_func]
end
end
---Creates an async function with a callback style function.
---@param func function: A callback style function to be converted. The last argument must be the callback.
---@param argc number: The number of arguments of func. Must be included.
---@return function: Returns an async function
M.wrap = function(func, argc)
if type(func) ~= "function" then
traceback_error("type error :: expected func, got " .. type(func))
end
if type(argc) ~= "number" then
traceback_error("type error :: expected number, got " .. type(argc))
end
local function leaf(...)
local nargs = select("#", ...)
if nargs == argc then
return func(...)
else
return co.yield(func, argc, ...)
end
end
add_leaf_function(leaf, argc)
return leaf
end
---Use this to either run a future concurrently and then do something else
---or use it to run a future with a callback in a non async context
---@param async_function function
---@param callback function
M.run = function(async_function, callback)
if M.is_leaf_function(async_function) then
async_function(callback)
else
execute(async_function, callback)
end
end
---Use this to create a function which executes in an async context but
---called from a non-async context. Inherently this cannot return anything
---since it is non-blocking
---@param func function
M.void = function(func)
return function(...)
execute(func, nil, ...)
end
end
return M

View File

@ -0,0 +1,218 @@
local a = require "plenary.async.async"
local Deque = require("plenary.async.structs").Deque
local tbl = require "plenary.tbl"
local M = {}
local Condvar = {}
Condvar.__index = Condvar
---@class Condvar
---@return Condvar
function Condvar.new()
return setmetatable({ handles = {} }, Condvar)
end
---`blocks` the thread until a notification is received
Condvar.wait = a.wrap(function(self, callback)
-- not calling the callback will block the coroutine
table.insert(self.handles, callback)
end, 2)
---notify everyone that is waiting on this Condvar
function Condvar:notify_all()
for i, callback in ipairs(self.handles) do
callback()
self.handles[i] = nil
end
end
---notify randomly one person that is waiting on this Condvar
function Condvar:notify_one()
if #self.handles == 0 then
return
end
local idx = math.random(#self.handles)
self.handles[idx]()
table.remove(self.handles, idx)
end
M.Condvar = Condvar
local Semaphore = {}
Semaphore.__index = Semaphore
---@class Semaphore
---@param initial_permits number: the number of permits that it can give out
---@return Semaphore
function Semaphore.new(initial_permits)
vim.validate {
initial_permits = {
initial_permits,
function(n)
return n > 0
end,
"number greater than 0",
},
}
return setmetatable({ permits = initial_permits, handles = {} }, Semaphore)
end
---async function, blocks until a permit can be acquired
---example:
---local semaphore = Semaphore.new(1024)
---local permit = await(semaphore:acquire())
---permit:forget()
---when a permit can be acquired returns it
---call permit:forget() to forget the permit
Semaphore.acquire = a.wrap(function(self, callback)
if self.permits > 0 then
self.permits = self.permits - 1
else
table.insert(self.handles, callback)
return
end
local permit = {}
permit.forget = function(self_permit)
self.permits = self.permits + 1
if self.permits > 0 and #self.handles > 0 then
self.permits = self.permits - 1
local callback = table.remove(self.handles)
callback(self_permit)
end
end
callback(permit)
end, 2)
M.Semaphore = Semaphore
M.channel = {}
---Creates a oneshot channel
---returns a sender and receiver function
---the sender is not async while the receiver is
---@return function, function
M.channel.oneshot = function()
local val = nil
local saved_callback = nil
local sent = false
local received = false
local is_single = false
--- sender is not async
--- sends a value which can be nil
local sender = function(...)
assert(not sent, "Oneshot channel can only send once")
sent = true
if saved_callback ~= nil then
saved_callback(...)
return
end
-- optimise for when there is only one or zero argument, no need to pack
local nargs = select("#", ...)
if nargs == 1 or nargs == 0 then
val = ...
is_single = true
else
val = tbl.pack(...)
end
end
--- receiver is async
--- blocks until a value is received
local receiver = a.wrap(function(callback)
assert(not received, "Oneshot channel can only receive one value!")
if sent then
received = true
if is_single then
return callback(val)
else
return callback(tbl.unpack(val))
end
else
saved_callback = callback
end
end, 1)
return sender, receiver
end
---A counter channel.
---Basically a channel that you want to use only to notify and not to send any actual values.
---@return function: sender
---@return function: receiver
M.channel.counter = function()
local counter = 0
local condvar = Condvar.new()
local Sender = {}
function Sender:send()
counter = counter + 1
condvar:notify_all()
end
local Receiver = {}
Receiver.recv = function()
if counter == 0 then
await(condvar:wait())
end
counter = counter - 1
end
Receiver.last = function()
if counter == 0 then
await(condvar:wait())
end
counter = 0
end
return Sender, Receiver
end
---A multiple producer single consumer channel
---@return table
---@return table
M.channel.mpsc = function()
local deque = Deque.new()
local condvar = Condvar.new()
local Sender = {}
function Sender.send(...)
deque:pushleft { ... }
condvar:notify_all()
end
local Receiver = {}
Receiver.recv = function()
if deque:is_empty() then
condvar:wait()
end
return unpack(deque:popright())
end
Receiver.last = function()
if deque:is_empty() then
condvar:wait()
end
local val = deque:popright()
deque:clear()
return unpack(val or {})
end
return Sender, Receiver
end
return M

View File

@ -0,0 +1,61 @@
---@brief [[
--- NOTE: This API is still under construction.
--- It may change in the future :)
---@brief ]]
local lookups = {
uv = "plenary.async.uv_async",
util = "plenary.async.util",
lsp = "plenary.async.lsp",
api = "plenary.async.api",
tests = "plenary.async.tests",
control = "plenary.async.control",
}
local exports = setmetatable(require "plenary.async.async", {
__index = function(t, k)
local require_path = lookups[k]
if not require_path then
return
end
local mod = require(require_path)
t[k] = mod
return mod
end,
})
exports.tests.add_globals = function()
a = exports
async = exports.async
await = exports.await
await_all = exports.await_all
-- must prefix with a or stack overflow, plenary.test harness already added it
a.describe = exports.tests.describe
-- must prefix with a or stack overflow
a.it = exports.tests.it
a.before_each = exports.tests.before_each
a.after_each = exports.tests.after_each
end
exports.tests.add_to_env = function()
local env = getfenv(2)
env.a = exports
env.async = exports.async
env.await = exports.await
env.await_all = exports.await_all
-- must prefix with a or stack overflow, plenary.test harness already added it
env.a.describe = exports.tests.describe
-- must prefix with a or stack overflow
env.a.it = exports.tests.it
a.before_each = exports.tests.before_each
a.after_each = exports.tests.after_each
setfenv(2, env)
end
return exports

View File

@ -0,0 +1,12 @@
local a = require "plenary.async.async"
local M = {}
---This will be deprecated because the callback can be called multiple times.
---This will give a coroutine error because the coroutine will be resumed multiple times.
---Please use buf_request_all instead.
M.buf_request = a.wrap(vim.lsp.buf_request, 4)
M.buf_request_all = a.wrap(vim.lsp.buf_request_all, 4)
return M

View File

@ -0,0 +1,116 @@
local M = {}
Deque = {}
Deque.__index = Deque
---@class Deque
---A double ended queue
---
---@return Deque
function Deque.new()
-- the indexes are created with an offset so that the indices are consequtive
-- otherwise, when both pushleft and pushright are used, the indices will have a 1 length hole in the middle
return setmetatable({ first = 0, last = -1 }, Deque)
end
---push to the left of the deque
---@param value any
function Deque:pushleft(value)
local first = self.first - 1
self.first = first
self[first] = value
end
---push to the right of the deque
---@param value any
function Deque:pushright(value)
local last = self.last + 1
self.last = last
self[last] = value
end
---pop from the left of the deque
---@return any
function Deque:popleft()
local first = self.first
if first > self.last then
return nil
end
local value = self[first]
self[first] = nil -- to allow garbage collection
self.first = first + 1
return value
end
---pops from the right of the deque
---@return any
function Deque:popright()
local last = self.last
if self.first > last then
return nil
end
local value = self[last]
self[last] = nil -- to allow garbage collection
self.last = last - 1
return value
end
---checks if the deque is empty
---@return boolean
function Deque:is_empty()
return self:len() == 0
end
---returns the number of elements of the deque
---@return number
function Deque:len()
return self.last - self.first + 1
end
---returns and iterator of the indices and values starting from the left
---@return function
function Deque:ipairs_left()
local i = self.first
return function()
local res = self[i]
local idx = i
if res then
i = i + 1
return idx, res
end
end
end
---returns and iterator of the indices and values starting from the right
---@return function
function Deque:ipairs_right()
local i = self.last
return function()
local res = self[i]
local idx = i
if res then
i = i - 1 -- advance the iterator before we return
return idx, res
end
end
end
---removes all values from the deque
---@return nil
function Deque:clear()
for i, _ in self:ipairs_left() do
self[i] = nil
end
self.first = 0
self.last = -1
end
M.Deque = Deque
return M

View File

@ -0,0 +1,21 @@
local util = require "plenary.async.util"
local M = {}
M.describe = function(s, async_func)
describe(s, async_func)
end
M.it = function(s, async_func)
it(s, util.will_block(async_func))
end
M.before_each = function(async_func)
before_each(util.will_block(async_func))
end
M.after_each = function(async_func)
after_each(util.will_block(async_func))
end
return M

View File

@ -0,0 +1,125 @@
local a = require "plenary.async.async"
local vararg = require "plenary.vararg"
-- local control = a.control
local control = require "plenary.async.control"
local channel = control.channel
local M = {}
local defer_swapped = function(timeout, callback)
vim.defer_fn(callback, timeout)
end
---Sleep for milliseconds
---@param ms number
M.sleep = a.wrap(defer_swapped, 2)
---This will COMPLETELY block neovim
---please just use a.run unless you have a very special usecase
---for example, in plenary test_harness you must use this
---@param async_function Future
---@param timeout number: Stop blocking if the timeout was surpassed. Default 2000.
M.block_on = function(async_function, timeout)
async_function = M.protected(async_function)
local stat, ret
a.run(async_function, function(_stat, ...)
stat = _stat
ret = { ... }
end)
vim.wait(timeout or 2000, function()
return stat ~= nil
end, 20, false)
if stat == false then
error(string.format("Blocking on future timed out or was interrupted.\n%s", unpack(ret)))
end
return unpack(ret)
end
M.will_block = function(async_func)
return function()
M.block_on(async_func)
end
end
M.join = function(async_fns)
local len = #async_fns
local results = {}
local done = 0
local tx, rx = channel.oneshot()
for i, async_fn in ipairs(async_fns) do
assert(type(async_fn) == "function", "type error :: future must be function")
local cb = function(...)
results[i] = { ... }
done = done + 1
if done == len then
tx()
end
end
a.run(async_fn, cb)
end
rx()
return results
end
---Returns a future that when run will select the first async_function that finishes
---@param async_funs table: The async_function that you want to select
---@return ...
M.run_first = a.wrap(function(async_funs, step)
local ran = false
for _, future in ipairs(async_funs) do
assert(type(future) == "function", "type error :: future must be function")
local callback = function(...)
if not ran then
ran = true
step(...)
end
end
future(callback)
end
end, 2)
M.run_all = function(async_fns, callback)
a.run(function()
M.join(async_fns)
end, callback)
end
function M.apcall(async_fn, ...)
local nargs = a.get_leaf_function_argc(async_fn)
if nargs then
local tx, rx = channel.oneshot()
local stat, ret = pcall(async_fn, vararg.rotate(nargs, tx, ...))
if not stat then
return stat, ret
else
return stat, rx()
end
else
return pcall(async_fn, ...)
end
end
function M.protected(async_fn)
return function()
return M.apcall(async_fn)
end
end
---An async function that when called will yield to the neovim scheduler to be able to call the api.
M.scheduler = a.wrap(vim.schedule, 1)
return M

View File

@ -0,0 +1,82 @@
local a = require "plenary.async.async"
local uv = vim.loop
local M = {}
local function add(name, argc)
local success, ret = pcall(a.wrap, uv[name], argc)
if not success then
error("Failed to add function with name " .. name)
end
M[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)
-- add('fs_opendir', 3) -- TODO: fix this one
add("fs_readdir", 2)
add("fs_closedir", 2)
-- 'fs_statfs',
-- 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)
-- tcp
add("tcp_connect", 4)
-- 'tcp_close_reset',
-- pipe
add("pipe_connect", 3)
-- udp
add("udp_send", 5)
add("udp_recv_start", 2)
-- fs event (wip make into async await event)
-- fs poll event (wip make into async await event)
-- dns
add("getaddrinfo", 4)
add("getnameinfo", 2)
return M

View File

@ -0,0 +1,15 @@
local a = require "plenary.async_lib.async"
local async, await = a.async, a.await
return setmetatable({}, {
__index = function(t, k)
return async(function(...)
-- if we are in a fast event await the scheduler
if vim.in_fast_event() then
await(a.scheduler())
end
vim.api[k](...)
end)
end,
})

View File

@ -0,0 +1,213 @@
local co = coroutine
local errors = require "plenary.errors"
local traceback_error = errors.traceback_error
local f = require "plenary.functional"
local tbl = require "plenary.tbl"
local M = {}
---because we can't store varargs
local function callback_or_next(step, thread, callback, ...)
local stat = f.first(...)
if not stat then
error(string.format("The coroutine failed with this message: %s", f.second(...)))
end
if co.status(thread) == "dead" then
(callback or function() end)(select(2, ...))
else
assert(select("#", select(2, ...)) == 1, "expected a single return value")
local returned_future = f.second(...)
assert(type(returned_future) == "function", "type error :: expected func")
returned_future(step)
end
end
---@class Future
---Something that will give a value when run
---Executes a future with a callback when it is done
---@param future Future: the future to execute
---@param callback function: the callback to call when done
local execute = function(future, callback)
assert(type(future) == "function", "type error :: expected func")
local thread = co.create(future)
local step
step = function(...)
callback_or_next(step, thread, callback, co.resume(thread, ...))
end
step()
end
---Creates an async function with a callback style function.
---@param func function: A callback style function to be converted. The last argument must be the callback.
---@param argc number: The number of arguments of func. Must be included.
---@return function: Returns an async function
M.wrap = function(func, argc)
if type(func) ~= "function" then
traceback_error("type error :: expected func, got " .. type(func))
end
if type(argc) ~= "number" and argc ~= "vararg" then
traceback_error "expected argc to be a number or string literal 'vararg'"
end
return function(...)
local params = tbl.pack(...)
local function future(step)
if step then
if type(argc) == "number" then
params[argc] = step
params.n = argc
else
table.insert(params, step) -- change once not optional
params.n = params.n + 1
end
return func(tbl.unpack(params))
else
return co.yield(future)
end
end
return future
end
end
---Return a new future that when run will run all futures concurrently.
---@param futures table: the futures that you want to join
---@return Future: returns a future
M.join = M.wrap(function(futures, step)
local len = #futures
local results = {}
local done = 0
if len == 0 then
return step(results)
end
for i, future in ipairs(futures) do
assert(type(future) == "function", "type error :: future must be function")
local callback = function(...)
results[i] = { ... }
done = done + 1
if done == len then
step(results)
end
end
future(callback)
end
end, 2)
---Returns a future that when run will select the first future that finishes
---@param futures table: The future that you want to select
---@return Future
M.select = M.wrap(function(futures, step)
local selected = false
for _, future in ipairs(futures) do
assert(type(future) == "function", "type error :: future must be function")
local callback = function(...)
if not selected then
selected = true
step(...)
end
end
future(callback)
end
end, 2)
---Use this to either run a future concurrently and then do something else
---or use it to run a future with a callback in a non async context
---@param future Future
---@param callback function
M.run = function(future, callback)
future(callback or function() end)
end
---Same as run but runs multiple futures
---@param futures table
---@param callback function
M.run_all = function(futures, callback)
M.run(M.join(futures), callback)
end
---Await a future, yielding the current function
---@param future Future
---@return any: returns the result of the future when it is done
M.await = function(future)
assert(type(future) == "function", "type error :: expected function to await")
return future(nil)
end
---Same as await but can await multiple futures.
---If the futures have libuv leaf futures they will be run concurrently
---@param futures table
---@return table: returns a table of results that each future returned. Note that if the future returns multiple values they will be packed into a table.
M.await_all = function(futures)
assert(type(futures) == "table", "type error :: expected table")
return M.await(M.join(futures))
end
---suspend a coroutine
M.suspend = co.yield
---create a async scope
M.scope = function(func)
M.run(M.future(func))
end
--- Future a :: a -> (a -> ())
--- turns this signature
--- ... -> Future a
--- into this signature
--- ... -> ()
M.void = function(async_func)
return function(...)
async_func(...)(function() end)
end
end
M.async_void = function(func)
return M.void(M.async(func))
end
---creates an async function
---@param func function
---@return function: returns an async function
M.async = function(func)
if type(func) ~= "function" then
traceback_error("type error :: expected func, got " .. type(func))
end
return function(...)
local args = tbl.pack(...)
local function future(step)
if step == nil then
return func(tbl.unpack(args))
else
execute(future, step)
end
end
return future
end
end
---creates a future
---@param func function
---@return Future
M.future = function(func)
return M.async(func)()
end
---An async function that when awaited will await the scheduler to be able to call the api.
M.scheduler = M.wrap(vim.schedule, 1)
return M

View File

@ -0,0 +1,41 @@
---@brief [[
--- NOTE: This API is still under construction.
--- It may change in the future :)
---@brief ]]
local exports = require "plenary.async_lib.async"
exports.uv = require "plenary.async_lib.uv_async"
exports.util = require "plenary.async_lib.util"
exports.lsp = require "plenary.async_lib.lsp"
exports.api = require "plenary.async_lib.api"
exports.tests = require "plenary.async_lib.tests"
exports.tests.add_globals = function()
a = exports
async = exports.async
await = exports.await
await_all = exports.await_all
-- must prefix with a or stack overflow, plenary.test harness already added it
a.describe = exports.tests.describe
-- must prefix with a or stack overflow
a.it = exports.tests.it
end
exports.tests.add_to_env = function()
local env = getfenv(2)
env.a = exports
env.async = exports.async
env.await = exports.await
env.await_all = exports.await_all
-- must prefix with a or stack overflow, plenary.test harness already added it
env.a.describe = exports.tests.describe
-- must prefix with a or stack overflow
env.a.it = exports.tests.it
setfenv(2, env)
end
return exports

View File

@ -0,0 +1,15 @@
local a = require "plenary.async_lib.async"
local M = {}
---This will be deprecated because the callback can be called multiple times.
---This will give a coroutine error because the coroutine will be resumed multiple times.
---Please use buf_request_all instead.
M.buf_request = a.wrap(vim.lsp.buf_request, 4)
--This was recently merged into master so we just check if it is there
if vim.lsp.buf_request_all ~= nil then
M.buf_request_all = a.wrap(vim.lsp.buf_request_all, 4)
end
return M

View File

@ -0,0 +1,116 @@
local M = {}
Deque = {}
Deque.__index = Deque
---@class Deque
---A double ended queue
---
---@return Deque
function Deque.new()
-- the indexes are created with an offset so that the indices are consequtive
-- otherwise, when both pushleft and pushright are used, the indices will have a 1 length hole in the middle
return setmetatable({ first = 0, last = -1 }, Deque)
end
---push to the left of the deque
---@param value any
function Deque:pushleft(value)
local first = self.first - 1
self.first = first
self[first] = value
end
---push to the right of the deque
---@param value any
function Deque:pushright(value)
local last = self.last + 1
self.last = last
self[last] = value
end
---pop from the left of the deque
---@return any
function Deque:popleft()
local first = self.first
if first > self.last then
return nil
end
local value = self[first]
self[first] = nil -- to allow garbage collection
self.first = first + 1
return value
end
---pops from the right of the deque
---@return any
function Deque:popright()
local last = self.last
if self.first > last then
return nil
end
local value = self[last]
self[last] = nil -- to allow garbage collection
self.last = last - 1
return value
end
---checks if the deque is empty
---@return boolean
function Deque:is_empty()
return self:len() == 0
end
---returns the number of elements of the deque
---@return number
function Deque:len()
return self.last - self.first + 1
end
---returns and iterator of the indices and values starting from the left
---@return function
function Deque:ipairs_left()
local i = self.first
return function()
local res = self[i]
local idx = i
if res then
i = i + 1
return idx, res
end
end
end
---returns and iterator of the indices and values starting from the right
---@return function
function Deque:ipairs_right()
local i = self.last
return function()
local res = self[i]
local idx = i
if res then
i = i - 1 -- advance the iterator before we return
return idx, res
end
end
end
---removes all values from the deque
---@return nil
function Deque:clear()
for i, _ in self:ipairs_left() do
self[i] = nil
end
self.first = 0
self.last = -1
end
M.Deque = Deque
return M

View File

@ -0,0 +1,14 @@
local a = require "plenary.async_lib.async"
local util = require "plenary.async_lib.util"
local M = {}
M.describe = function(s, func)
describe(s, util.will_block(a.future(func)))
end
M.it = function(s, func)
it(s, util.will_block(a.future(func)))
end
return M

View File

@ -0,0 +1,339 @@
local a = require "plenary.async_lib.async"
local await = a.await
local async = a.async
local co = coroutine
local Deque = require("plenary.async_lib.structs").Deque
local uv = vim.loop
local M = {}
---Sleep for milliseconds
---@param ms number
M.sleep = a.wrap(function(ms, callback)
local timer = uv.new_timer()
uv.timer_start(timer, ms, 0, function()
uv.timer_stop(timer)
uv.close(timer)
callback()
end)
end, 2)
---Takes a future and a millisecond as the timeout.
---If the time is reached and the future hasn't completed yet, it will short circuit the future
---NOTE: the future will still be running in libuv, we are just not waiting for it to complete
---thats why you should call this on a leaf future only to avoid unexpected results
---@param future Future
---@param ms number
M.timeout = a.wrap(function(future, ms, callback)
-- make sure that the callback isn't called twice, or else the coroutine can be dead
local done = false
local timeout_callback = function(...)
if not done then
done = true
callback(false, ...) -- false because it has run normally
end
end
vim.defer_fn(function()
if not done then
done = true
callback(true) -- true because it has timed out
end
end, ms)
a.run(future, timeout_callback)
end, 3)
---create an async function timer
---@param ms number
M.timer = function(ms)
return async(function()
await(M.sleep(ms))
end)
end
---id function that can be awaited
---@param nil ...
---@return ...
M.id = async(function(...)
return ...
end)
---Running this function will yield now and do nothing else
M.yield_now = async(function()
await(M.id())
end)
local Condvar = {}
Condvar.__index = Condvar
---@class Condvar
---@return Condvar
function Condvar.new()
return setmetatable({ handles = {} }, Condvar)
end
---`blocks` the thread until a notification is received
Condvar.wait = a.wrap(function(self, callback)
-- not calling the callback will block the coroutine
table.insert(self.handles, callback)
end, 2)
---notify everyone that is waiting on this Condvar
function Condvar:notify_all()
if #self.handles == 0 then
return
end
for i, callback in ipairs(self.handles) do
callback()
self.handles[i] = nil
end
end
---notify randomly one person that is waiting on this Condvar
function Condvar:notify_one()
if #self.handles == 0 then
return
end
local idx = math.random(#self.handles)
self.handles[idx]()
table.remove(self.handles, idx)
end
M.Condvar = Condvar
local Semaphore = {}
Semaphore.__index = Semaphore
---@class Semaphore
---@param initial_permits number: the number of permits that it can give out
---@return Semaphore
function Semaphore.new(initial_permits)
vim.validate {
initial_permits = {
initial_permits,
function(n)
return n > 0
end,
"number greater than 0",
},
}
return setmetatable({ permits = initial_permits, handles = {} }, Semaphore)
end
---async function, blocks until a permit can be acquired
---example:
---local semaphore = Semaphore.new(1024)
---local permit = await(semaphore:acquire())
---permit:forget()
---when a permit can be acquired returns it
---call permit:forget() to forget the permit
Semaphore.acquire = a.wrap(function(self, callback)
if self.permits > 0 then
self.permits = self.permits - 1
else
table.insert(self.handles, callback)
return
end
local permit = {}
permit.forget = function(self_permit)
self.permits = self.permits + 1
if self.permits > 0 and #self.handles > 0 then
self.permits = self.permits - 1
local callback = table.remove(self.handles)
callback(self_permit)
end
end
callback(permit)
end, 2)
M.Semaphore = Semaphore
M.channel = {}
---Creates a oneshot channel
---returns a sender and receiver function
---the sender is not async while the receiver is
---@return function, function
M.channel.oneshot = function()
local val = nil
local saved_callback = nil
local sent = false
local received = false
--- sender is not async
--- sends a value
local sender = function(...)
if sent then
error "Oneshot channel can only send once"
end
sent = true
local args = { ... }
if saved_callback then
saved_callback(unpack(val or args))
else
val = args
end
end
--- receiver is async
--- blocks until a value is received
local receiver = a.wrap(function(callback)
if received then
error "Oneshot channel can only send one value!"
end
if val then
received = true
callback(unpack(val))
else
saved_callback = callback
end
end, 1)
return sender, receiver
end
---A counter channel.
---Basically a channel that you want to use only to notify and not to send any actual values.
---@return function: sender
---@return function: receiver
M.channel.counter = function()
local counter = 0
local condvar = Condvar.new()
local Sender = {}
function Sender:send()
counter = counter + 1
condvar:notify_all()
end
local Receiver = {}
Receiver.recv = async(function()
if counter == 0 then
await(condvar:wait())
end
counter = counter - 1
end)
Receiver.last = async(function()
if counter == 0 then
await(condvar:wait())
end
counter = 0
end)
return Sender, Receiver
end
---A multiple producer single consumer channel
---@return table
---@return table
M.channel.mpsc = function()
local deque = Deque.new()
local condvar = Condvar.new()
local Sender = {}
function Sender.send(...)
deque:pushleft { ... }
condvar:notify_all()
end
local Receiver = {}
Receiver.recv = async(function()
if deque:is_empty() then
await(condvar:wait())
end
return unpack(deque:popright())
end)
Receiver.last = async(function()
if deque:is_empty() then
await(condvar:wait())
end
local val = deque:popright()
deque:clear()
return unpack(val)
end)
return Sender, Receiver
end
local pcall_wrap = function(func)
return function(...)
return pcall(func, ...)
end
end
---Makes a future protected. It is like pcall but for futures.
---Only works for non-leaf futures
M.protected_non_leaf = async(function(future)
return await(pcall_wrap(future))
end)
---Makes a future protected. It is like pcall but for futures.
---@param future Future
---@return Future
M.protected = async(function(future)
local tx, rx = M.channel.oneshot()
stat, ret = pcall(future, tx)
if stat == true then
return stat, await(rx())
else
return stat, ret
end
end)
---This will COMPLETELY block neovim
---please just use a.run unless you have a very special usecase
---for example, in plenary test_harness you must use this
---@param future Future
---@param timeout number: Stop blocking if the timeout was surpassed. Default 2000.
M.block_on = function(future, timeout)
future = M.protected(future)
local stat, ret
a.run(future, function(_stat, ...)
stat = _stat
ret = { ... }
end)
local function check()
if stat == false then
error("Blocking on future failed " .. unpack(ret))
end
return stat == true
end
if not vim.wait(timeout or 2000, check, 20, false) then
error "Blocking on future timed out or was interrupted"
end
return unpack(ret)
end
---Returns a new future that WILL BLOCK
---@param future Future
---@return Future
M.will_block = async(function(future)
return M.block_on(future)
end)
return M

View File

@ -0,0 +1,82 @@
local a = require "plenary.async_lib.async"
local uv = vim.loop
local M = {}
local function add(name, argc)
local success, ret = pcall(a.wrap, uv[name], argc)
if not success then
error("Failed to add function with name " .. name)
end
M[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)
-- add('fs_opendir', 3) -- TODO: fix this one
add("fs_readdir", 2)
add("fs_closedir", 2)
-- 'fs_statfs',
-- 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)
-- tcp
add("tcp_connect", 4)
-- 'tcp_close_reset',
-- pipe
add("pipe_connect", 3)
-- udp
add("udp_send", 5)
add("udp_recv_start", 2)
-- fs event (wip make into async await event)
-- fs poll event (wip make into async await event)
-- dns
add("getaddrinfo", 4)
add("getnameinfo", 2)
return M

View File

@ -0,0 +1,126 @@
local B = {}
local stat = require "plenary.benchmark.stat"
local get_stats = function(results)
local ret = {}
ret.max, ret.min = stat.maxmin(results)
ret.mean = stat.mean(results)
ret.median = stat.median(results)
ret.std = stat.std_dev(results)
return ret
end
local get_output = function(index, res, runs)
-- divine with a sutable one / 1e3, 1e6, 1e9
local time_types = { "ns", "μs", "ms" }
local get_leading = function(time)
time = math.floor(time)
local count = 0
repeat
time = math.floor(time / 10)
count = count + 1
until time <= 0
return count
end
local get_best_fmt = function(time)
for _, v in ipairs(time_types) do
if math.abs(time) < 1000.0 then
return string.format("%s%3.1f %s", string.rep(" ", 3 - get_leading(time)), time, v)
end
time = time / 1000.0
end
return string.format("%.1f %s", time, "s")
end
return string.format(
"Benchmark #%d: '%s'\n Time(mean ± σ): %s ± %s\n Range(min … max): %s … %s %d runs\n",
index,
res.name,
get_best_fmt(res.stats.mean),
get_best_fmt(res.stats.std),
get_best_fmt(res.stats.min),
get_best_fmt(res.stats.max),
runs
)
end
local get_summary = function(res)
if #res == 1 then
return ""
end
local fastest_mean = math.huge
local fastest_index = 1
for i, benchmark in ipairs(res) do
if benchmark.stats.mean < fastest_mean then
fastest_mean = benchmark.stats.mean
fastest_index = i
end
end
if fastest_mean == math.huge then
return ""
end
local output = {}
local fastest = res[fastest_index].stats
for i, benchmark in ipairs(res) do
if i ~= fastest_index then
local result = benchmark.stats
local ratio = result.mean / fastest.mean
-- // https://en.wikipedia.org/wiki/Propagation_of_uncertainty#Example_formulas
-- // Covariance asssumed to be 0, i.e. variables are assumed to be independent
local ratio_std = ratio
* math.sqrt(math.pow(result.std / result.mean, 2) + math.pow(fastest.std / fastest.mean, 2))
table.insert(output, string.format(" %.1f ± %.1f times faster than '%s'\n", ratio, ratio_std, benchmark.name))
end
end
return string.format("Summary\n '%s' ran\n%s", res[fastest_index].name, table.concat(output, ""))
end
---@class benchmark_run_opts
---@field warmup number @number of initial runs before starting to track time.
---@field runs number @number of runs to make
---@field fun table<array<string, function>> @functions to execute
---Benchmark a function
---@param name string @benchmark name
---@param opts benchmark_run_opts
local bench = function(name, opts)
vim.validate {
opts = { opts, "table" },
fun = { opts.fun, "table" },
}
opts.warmup = vim.F.if_nil(opts.warmup, 3)
opts.runs = vim.F.if_nil(opts.runs, 5)
opts.fun = type(opts.fun) == "function" and { opts.fun } or opts.fun
local output = { string.format("Benchmark Group: '%s' -----------------------\n", name) }
local res = {}
for i, fun in ipairs(opts.fun) do
res[i] = { name = fun[1], results = {} }
for _ = 1, opts.warmup do
fun[2]()
end
for j = 1, opts.runs do
local start = vim.loop.hrtime()
fun[2]()
res[i].results[j] = vim.loop.hrtime() - start
end
res[i].stats = get_stats(res[i].results)
table.insert(output, get_output(i, res[i], opts.runs))
end
print(string.format("%s\n%s", table.concat(output, ""), get_summary(res)))
return res
end
return bench

View File

@ -0,0 +1,86 @@
local stat = {}
---Calculate mean
---@param t number[] @double
---@return number @double
stat.mean = function(t)
local sum = 0
local count = 0
for _, v in pairs(t) do
if type(v) == "number" then
sum = sum + v
count = count + 1
end
end
return (sum / count)
end
-- Get the median of a table.
---@param t number[]
---@return number
stat.median = function(t)
local temp = {}
-- deep copy table so that when we sort it, the original is unchanged
-- also weed out any non numbers
for _, v in pairs(t) do
if type(v) == "number" then
table.insert(temp, v)
end
end
table.sort(temp)
-- If we have an even number of table elements or odd.
if math.fmod(#temp, 2) == 0 then
-- return mean value of middle two elements
return (temp[#temp / 2] + temp[(#temp / 2) + 1]) / 2
else
-- return middle element
return temp[math.ceil(#temp / 2)]
end
end
--- Get the standard deviation of a table
---@param t number[]
stat.std_dev = function(t)
local m, vm, result
local sum = 0
local count = 0
m = stat.mean(t)
for _, v in pairs(t) do
if type(v) == "number" then
vm = v - m
sum = sum + (vm * vm)
count = count + 1
end
end
result = math.sqrt(sum / (count - 1))
return result
end
---Get the max and min for a table
---@param t number[]
---@return number
---@return number
stat.maxmin = function(t)
local max = -math.huge
local min = math.huge
for _, v in pairs(t) do
if type(v) == "number" then
max = math.max(max, v)
min = math.min(min, v)
end
end
return max, min
end
return stat

View File

@ -0,0 +1,341 @@
-- Shortcircuit to returning bit if it already exists
if bit then return bit end
--[[
Credit: https://github.com/davidm/lua-bit-numberlua/blob/master/lmod/bit/numberlua.lua
LUA MODULE
bit.numberlua - Bitwise operations implemented in pure Lua as numbers,
with Lua 5.2 'bit32' and (LuaJIT) LuaBitOp 'bit' compatibility interfaces.
SYNOPSIS
local bit = require 'bit.numberlua'
print(bit.band(0xff00ff00, 0x00ff00ff)) --> 0xffffffff
-- Interface providing strong (LuaJIT) LuaBitOp 'bit' compatibility
local bit = require 'plenary.bit'
assert(bit.tobit(0xffffffff) == -1)
REMOVED!
-- Interface providing strong Lua 5.2 'bit32' compatibility
local bit32 = require 'bit.numberlua'.bit32
assert(bit32.band(-1) == 0xffffffff)
DESCRIPTION
This library implements bitwise operations entirely in Lua.
This module is typically intended if for some reasons you don't want
to or cannot install a popular C based bit library like BitOp 'bit' [1]
(which comes pre-installed with LuaJIT) or 'bit32' (which comes
pre-installed with Lua 5.2) but want a similar interface.
This modules represents bit arrays as non-negative Lua numbers. [1]
It can represent 32-bit bit arrays when Lua is compiled
with lua_Number as double-precision IEEE 754 floating point.
The module is nearly the most efficient it can be but may be a few times
slower than the C based bit libraries and is orders or magnitude
slower than LuaJIT bit operations, which compile to native code. Therefore,
this library is inferior in performane to the other modules.
The `xor` function in this module is based partly on Roberto Ierusalimschy's
post in http://lua-users.org/lists/lua-l/2002-09/msg00134.html .
The included BIT.bit32 and BIT.bit sublibraries aims to provide 100%
compatibility with the Lua 5.2 "bit32" and (LuaJIT) LuaBitOp "bit" library.
This compatbility is at the cost of some efficiency since inputted
numbers are normalized and more general forms (e.g. multi-argument
bitwise operators) are supported.
STATUS
WARNING: Not all corner cases have been tested and documented.
Some attempt was made to make these similar to the Lua 5.2 [2]
and LuaJit BitOp [3] libraries, but this is not fully tested and there
are currently some differences. Addressing these differences may
be improved in the future but it is not yet fully determined how to
resolve these differences.
The BIT.bit32 library passes the Lua 5.2 test suite (bitwise.lua)
http://www.lua.org/tests/5.2/ . The BIT.bit library passes the LuaBitOp
test suite (bittest.lua). However, these have not been tested on
platforms with Lua compiled with 32-bit integer numbers.
API
Module's return
This table contains functions that aim to provide 100% compatibility
with the LuaBitOp "bit" library (from LuaJIT).
bit.tobit(x) --> y
bit.tohex(x [,n]) --> y
bit.bnot(x) --> y
bit.bor(x1 [,x2...]) --> y
bit.band(x1 [,x2...]) --> y
bit.bxor(x1 [,x2...]) --> y
bit.lshift(x, n) --> y
bit.rshift(x, n) --> y
bit.arshift(x, n) --> y
bit.rol(x, n) --> y
bit.ror(x, n) --> y
bit.bswap(x) --> y
DEPENDENCIES
None (other than Lua 5.1 or 5.2).
REFERENCES
[1] http://lua-users.org/wiki/FloatingPoint
[2] http://www.lua.org/manual/5.2/
[3] http://bitop.luajit.org/
LICENSE
(c) 2008-2011 David Manura. Licensed under the same terms as Lua (MIT).
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.
(end license)
Some modifications by plenary team.
--]]
local M = {_TYPE='module', _NAME='bit.numberlua', _VERSION='0.3.1.20120131'}
local floor = math.floor
local MOD = 2^32
local MODM = MOD-1
local function memoize(f)
local mt = {}
local t = setmetatable({}, mt)
function mt:__index(k)
local v = f(k); t[k] = v
return v
end
return t
end
local function make_bitop_uncached(t, m)
local function bitop(a, b)
local res,p = 0,1
while a ~= 0 and b ~= 0 do
local am, bm = a%m, b%m
res = res + t[am][bm]*p
a = (a - am) / m
b = (b - bm) / m
p = p*m
end
res = res + (a+b)*p
return res
end
return bitop
end
local function make_bitop(t)
local op1 = make_bitop_uncached(t,2^1)
local op2 = memoize(function(a)
return memoize(function(b)
return op1(a, b)
end)
end)
return make_bitop_uncached(op2, 2^(t.n or 1))
end
-- ok? probably not if running on a 32-bit int Lua number type platform
function M.tobit(x)
return x % 2^32
end
M.bxor = make_bitop {[0]={[0]=0,[1]=1},[1]={[0]=1,[1]=0}, n=4}
local bxor = M.bxor
function M.bnot(a) return MODM - a end
local bnot = M.bnot
function M.band(a,b) return ((a+b) - bxor(a,b))/2 end
local band = M.band
function M.bor(a,b) return MODM - band(MODM - a, MODM - b) end
local bor = M.bor
local lshift, rshift -- forward declare
function M.rshift(a,disp) -- Lua5.2 insipred
if disp < 0 then return lshift(a,-disp) end
return floor(a % 2^32 / 2^disp)
end
rshift = M.rshift
function M.lshift(a,disp) -- Lua5.2 inspired
if disp < 0 then return rshift(a,-disp) end
return (a * 2^disp) % 2^32
end
lshift = M.lshift
function M.tohex(x, n) -- BitOp style
n = n or 8
local up
if n <= 0 then
if n == 0 then return '' end
up = true
n = - n
end
x = band(x, 16^n-1)
return ('%0'..n..(up and 'X' or 'x')):format(x)
end
local tohex = M.tohex
function M.extract(n, field, width) -- Lua5.2 inspired
width = width or 1
return band(rshift(n, field), 2^width-1)
end
local extract = M.extract
function M.replace(n, v, field, width) -- Lua5.2 inspired
width = width or 1
local mask1 = 2^width-1
v = band(v, mask1) -- required by spec?
local mask = bnot(lshift(mask1, field))
return band(n, mask) + lshift(v, field)
end
local replace = M.replace
function M.bswap(x) -- BitOp style
local a = band(x, 0xff); x = rshift(x, 8)
local b = band(x, 0xff); x = rshift(x, 8)
local c = band(x, 0xff); x = rshift(x, 8)
local d = band(x, 0xff)
return lshift(lshift(lshift(a, 8) + b, 8) + c, 8) + d
end
local bswap = M.bswap
function M.rrotate(x, disp) -- Lua5.2 inspired
disp = disp % 32
local low = band(x, 2^disp-1)
return rshift(x, disp) + lshift(low, 32-disp)
end
local rrotate = M.rrotate
function M.lrotate(x, disp) -- Lua5.2 inspired
return rrotate(x, -disp)
end
local lrotate = M.lrotate
M.rol = M.lrotate -- LuaOp inspired
M.ror = M.rrotate -- LuaOp insipred
function M.arshift(x, disp) -- Lua5.2 inspired
local z = rshift(x, disp)
if x >= 0x80000000 then z = z + lshift(2^disp-1, 32-disp) end
return z
end
local arshift = M.arshift
function M.btest(x, y) -- Lua5.2 inspired
return band(x, y) ~= 0
end
--
-- Start LuaBitOp "bit" compat section.
--
M.bit = {} -- LuaBitOp "bit" compatibility
function M.bit.tobit(x)
x = x % MOD
if x >= 0x80000000 then x = x - MOD end
return x
end
local bit_tobit = M.bit.tobit
function M.bit.tohex(x, ...)
return tohex(x % MOD, ...)
end
function M.bit.bnot(x)
return bit_tobit(bnot(x % MOD))
end
local function bit_bor(a, b, c, ...)
if c then
return bit_bor(bit_bor(a, b), c, ...)
elseif b then
return bit_tobit(bor(a % MOD, b % MOD))
else
return bit_tobit(a)
end
end
M.bit.bor = bit_bor
local function bit_band(a, b, c, ...)
if c then
return bit_band(bit_band(a, b), c, ...)
elseif b then
return bit_tobit(band(a % MOD, b % MOD))
else
return bit_tobit(a)
end
end
M.bit.band = bit_band
local function bit_bxor(a, b, c, ...)
if c then
return bit_bxor(bit_bxor(a, b), c, ...)
elseif b then
return bit_tobit(bxor(a % MOD, b % MOD))
else
return bit_tobit(a)
end
end
M.bit.bxor = bit_bxor
function M.bit.lshift(x, n)
return bit_tobit(lshift(x % MOD, n % 32))
end
function M.bit.rshift(x, n)
return bit_tobit(rshift(x % MOD, n % 32))
end
function M.bit.arshift(x, n)
return bit_tobit(arshift(x % MOD, n % 32))
end
function M.bit.rol(x, n)
return bit_tobit(lrotate(x % MOD, n % 32))
end
function M.bit.ror(x, n)
return bit_tobit(rrotate(x % MOD, n % 32))
end
function M.bit.bswap(x)
return bit_tobit(bswap(x % MOD))
end
return M.bit

View File

@ -0,0 +1,264 @@
local dirname = function(p)
return vim.fn.fnamemodify(p, ":h")
end
local function get_trace(element, level, msg)
local function trimTrace(info)
local index = info.traceback:find "\n%s*%[C]"
info.traceback = info.traceback:sub(1, index)
return info
end
level = level or 3
local thisdir = dirname(debug.getinfo(1, "Sl").source, ":h")
local info = debug.getinfo(level, "Sl")
while
info.what == "C"
or info.short_src:match "luassert[/\\].*%.lua$"
or (info.source:sub(1, 1) == "@" and thisdir == dirname(info.source))
do
level = level + 1
info = debug.getinfo(level, "Sl")
end
info.traceback = debug.traceback("", level)
info.message = msg
-- local file = busted.getFile(element)
local file = false
return file and file.getTrace(file.name, info) or trimTrace(info)
end
local is_headless = require("plenary.nvim_meta").is_headless
-- We are shadowing print so people can reliably print messages
print = function(...)
for _, v in ipairs { ... } do
io.stdout:write(tostring(v))
io.stdout:write "\t"
end
io.stdout:write "\r\n"
end
local mod = {}
local results = {}
local current_description = {}
local current_before_each = {}
local current_after_each = {}
local add_description = function(desc)
table.insert(current_description, desc)
return vim.deepcopy(current_description)
end
local pop_description = function()
current_description[#current_description] = nil
end
local add_new_each = function()
current_before_each[current_description[#current_description]] = {}
current_after_each[current_description[#current_description]] = {}
end
local clear_last_each = function()
current_before_each[current_description[#current_description]] = nil
current_after_each[current_description[#current_description]] = nil
end
local call_inner = function(desc, func)
local desc_stack = add_description(desc)
add_new_each()
local ok, msg = xpcall(func, function(msg)
-- debug.traceback
-- return vim.inspect(get_trace(nil, 3, msg))
local trace = get_trace(nil, 3, msg)
return trace.message .. "\n" .. trace.traceback
end)
clear_last_each()
pop_description()
return ok, msg, desc_stack
end
local color_table = {
yellow = 33,
green = 32,
red = 31,
}
local color_string = function(color, str)
if not is_headless then
return str
end
return string.format("%s[%sm%s%s[%sm", string.char(27), color_table[color] or 0, str, string.char(27), 0)
end
local SUCCESS = color_string("green", "Success")
local FAIL = color_string("red", "Fail")
local PENDING = color_string("yellow", "Pending")
local HEADER = string.rep("=", 40)
mod.format_results = function(res)
print ""
print(color_string("green", "Success: "), #res.pass)
print(color_string("red", "Failed : "), #res.fail)
print(color_string("red", "Errors : "), #res.errs)
print(HEADER)
end
mod.describe = function(desc, func)
results.pass = results.pass or {}
results.fail = results.fail or {}
results.errs = results.errs or {}
describe = mod.inner_describe
local ok, msg, desc_stack = call_inner(desc, func)
describe = mod.describe
if not ok then
table.insert(results.errs, {
descriptions = desc_stack,
msg = msg,
})
end
end
mod.inner_describe = function(desc, func)
local ok, msg, desc_stack = call_inner(desc, func)
if not ok then
table.insert(results.errs, {
descriptions = desc_stack,
msg = msg,
})
end
end
mod.before_each = function(fn)
table.insert(current_before_each[current_description[#current_description]], fn)
end
mod.after_each = function(fn)
table.insert(current_after_each[current_description[#current_description]], fn)
end
mod.clear = function()
vim.api.nvim_buf_set_lines(0, 0, -1, false, {})
end
local indent = function(msg, spaces)
if spaces == nil then
spaces = 4
end
local prefix = string.rep(" ", spaces)
return prefix .. msg:gsub("\n", "\n" .. prefix)
end
local run_each = function(tbl)
for _, v in pairs(tbl) do
for _, w in ipairs(v) do
if type(w) == "function" then
w()
end
end
end
end
mod.it = function(desc, func)
run_each(current_before_each)
local ok, msg, desc_stack = call_inner(desc, func)
run_each(current_after_each)
local test_result = {
descriptions = desc_stack,
msg = nil,
}
-- TODO: We should figure out how to determine whether
-- and assert failed or whether it was an error...
local to_insert, printed
if not ok then
to_insert = results.fail
test_result.msg = msg
print(FAIL, "||", table.concat(test_result.descriptions, " "))
print(indent(msg, 12))
else
to_insert = results.pass
print(SUCCESS, "||", table.concat(test_result.descriptions, " "))
end
table.insert(to_insert, test_result)
end
mod.pending = function(desc, func)
local curr_stack = vim.deepcopy(current_description)
table.insert(curr_stack, desc)
print(PENDING, "||", table.concat(curr_stack, " "))
end
_PlenaryBustedOldAssert = _PlenaryBustedOldAssert or assert
describe = mod.describe
it = mod.it
pending = mod.pending
before_each = mod.before_each
after_each = mod.after_each
clear = mod.clear
assert = require "luassert"
mod.run = function(file)
print("\n" .. HEADER)
print("Testing: ", file)
local ok, msg = pcall(dofile, file)
if not ok then
print(HEADER)
print "FAILED TO LOAD FILE"
print(color_string("red", msg))
print(HEADER)
if is_headless then
return vim.cmd "2cq"
else
return
end
end
-- If nothing runs (empty file without top level describe)
if not results.pass then
if is_headless then
return vim.cmd "0cq"
else
return
end
end
mod.format_results(results)
if #results.errs ~= 0 then
print("We had an unexpected error: ", vim.inspect(results.errs), vim.inspect(results))
if is_headless then
return vim.cmd "2cq"
end
elseif #results.fail > 0 then
print "Tests Failed. Exit: 1"
if is_headless then
return vim.cmd "1cq"
end
else
if is_headless then
return vim.cmd "0cq"
end
end
end
return mod

View File

@ -0,0 +1,80 @@
---@brief [[
---classic
---
---Copyright (c) 2014, rxi
---@brief ]]
---@class Object
local Object = {}
Object.__index = Object
---Does nothing.
---You have to implement this yourself for extra functionality when initializing
---@param self Object
function Object:new() end
---Create a new class/object by extending the base Object class.
---The extended object will have a field called `super` that will access the super class.
---@param self Object
---@return Object
function Object:extend()
local cls = {}
for k, v in pairs(self) do
if k:find "__" == 1 then
cls[k] = v
end
end
cls.__index = cls
cls.super = self
setmetatable(cls, self)
return cls
end
---Implement a mixin onto this Object.
---@param self Object
---@param nil ...
function Object:implement(...)
for _, cls in pairs { ... } do
for k, v in pairs(cls) do
if self[k] == nil and type(v) == "function" then
self[k] = v
end
end
end
end
---Checks if the object is an instance
---This will start with the lowest class and loop over all the superclasses.
---@param self Object
---@param T Object
---@return boolean
function Object:is(T)
local mt = getmetatable(self)
while mt do
if mt == T then
return true
end
mt = getmetatable(mt)
end
return false
end
---The default tostring implementation for an object.
---You can override this to provide a different tostring.
---@param self Object
---@return string
function Object:__tostring()
return "Object"
end
---You can call the class the initialize it without using `Object:new`.
---@param self Object
---@param nil ...
---@return Object
function Object:__call(...)
local obj = setmetatable({}, self)
obj:new(...)
return obj
end
return Object

View File

@ -0,0 +1,393 @@
---@brief [[
--- This module implements python-like lists. It can be used like so:
--- <pre>
--- local List = require 'plenary.collections.py_list'
--- local l = List{3, 20, 44}
--- print(l) -- [3, 20, 44]
--- </pre>
---@brief ]]
local List = {}
---@class List @The base class for all list objects
---List constructor. Can be used in higher order functions
---@param tbl table: A list-like table containing the initial elements of the list
---@return List: A new list object
function List.new(tbl)
if type(tbl) == "table" then
local len = #tbl
local obj = setmetatable(tbl, List)
obj._len = len
return obj
end
error "List constructor must be called with table argument"
end
--- Checks whether the argument is a List object
--- @param tbl table: The object to test
--- @return boolean: Whether tbl is an instance of List
function List.is_list(tbl)
local meta = getmetatable(tbl) or {}
return meta == List
end
function List:__index(key)
if self ~= List then
local field = List[key]
if field then
return field
end
end
end
-- TODO: Similar to python, use [...] if the table references itself --
function List:__tostring()
local elements = self:join ", "
return "[" .. elements .. "]"
end
function List:__eq(other)
if #self ~= #other then
return false
end
for i = 1, #self do
if self[i] ~= other[i] then
return false
end
end
return true
end
function List:__mul(other)
local result = List.new {}
for i = 1, other do
result[i] = self
end
return result
end
function List:__len()
return self._len
end
function List:__concat(other)
return self:concat(other)
end
--- Pushes the element to the end of the list
--- @param other any: The object to append
--- @see List.pop
function List:push(other)
self[#self + 1] = other
self._len = self._len + 1
end
--- Pops the last element off the list and returns it
--- @return any: The (previously) last element from the list
--- @see List.push
function List:pop()
local result = table.remove(self)
self._len = self._len - 1
return result
end
--- Inserts other into the specified idx
--- @param idx number: The index that other will be inserted to
--- @param other any: The element to insert
--- @see List.remove
function List:insert(idx, other)
table.insert(self, idx, other)
self._len = self._len + 1
end
--- Removes the element at index idx and returns it
--- @param idx number: The index of the element to remove
--- @return any: The element previously at index idx
--- @see List.insert
function List:remove(idx)
self._len = self._len - 1
return table.remove(self, idx)
end
--- Can be used to compare elements with any list-like table. It only checks for
--- shallow equality
--- @param other any: The element to test for
--- @return boolean: True if other is a list object and all it's elements are equal
--- @see List.deep_equal
function List:equal(other)
return self:__eq(other)
end
--- Checks for deep equality between lists. This uses vim.deep_equal for testing
--- @param other any: The element to test for
--- @return boolean: True if all elements and their children are equal
--- @see List.equal
--- @see vim.deep_equal
function List:deep_equal(other)
return vim.deep_equal(self, other)
end
--- Returns a copy of the list with elements between a and b, inclusive
--- <pre>
--- local list = List{1, 2, 3, 4}
--- local slice = list:slice(2, 3)
--- print(slice) -- [2, 3]
--- </pre>
--- @param a number: The low end of the slice
--- @param b number: The high end of the slice
--- @return List: A list with elements between a and b
function List:slice(a, b)
return List.new(vim.list_slice(self, a, b))
end
--- Similar to slice, but with every element. It only makes a shallow copy
--- @return List: A slice from 1 to #self, i.e., a complete copy of the list
--- @see List.deep_copy
function List:copy()
return self:slice(1, #self)
end
--- Similar to copy, but makes a deep copy instead
--- @return List: A deep copy of the object
--- @see List.copy
--- @see vim.deep_copy
function List:deep_copy()
return vim.deep_copy(self)
end
--- Reverses the list in place. If you don't want this, you could do something
--- like this
--- <pre>
--- local list = List{1, 2, 3, 4}
--- local reversed = list:copy():reverse()
--- </pre>
--- @return List: The list itself, so you can chain method calls
--- @see List.copy
--- @see List.deep_copy
function List:reverse()
local n = #self
local i = 1
while i < n do
self[i], self[n] = self[n], self[i]
i = i + 1
n = n - 1
end
return self
end
--- Concatenates the elements whithin the list separated by the given string
--- <pre>
--- local list = List{1, 2, 3, 4}
--- print(list:join('-')) -- 1-2-3-4
--- </pre>
--- @param sep string: The separator to place between the elements. Default ''
--- @return string: The elements in the list separated by sep
function List:join(sep)
sep = sep or ""
local result = ""
for i, v in self:iter() do
result = result .. tostring(v)
if i ~= #self then
result = result .. sep
end
end
return result
end
--- Returns a list with the elements of self concatenated with those in the
--- given arguments
--- @vararg table|List: The sequences to concatenate to this one
--- @return List
function List:concat(...)
local result = self:copy()
local others = { ... }
for _, other in ipairs(others) do
for _, v in ipairs(other) do
result:push(v)
end
end
return result
end
--- Moves the elements between from and from+len in self, to positions between
--- to and to+len in other, like so
--- <pre>
--- other[to], other[to+1]... other[to+len] = self[from], self[from+1]... self[from+len]
--- </pre>
--- @param from number: The first index of the origin slice
--- @param len number: The length of the slices
--- @param to number: The first index of the destination slice
--- @param other table|List: The destination list. Defaults to self
--- @see table.move
function List:move(from, len, to, other)
return table.move(self, from, len, to, other)
end
--- Packs the given elements into a list. Similar to lua 5.3's table.pack
--- @vararg any: The elements to pack
--- @return List: a list containing all the given elements
--- @see table.pack
function List.pack(...)
return List.new { ... }
end
--- Unpacks the elements from this list and returns them
--- @return ...any: All the elements from self[1] to self[#self]
function List:unpack()
return unpack(self, 1, #self)
end
-- Iterator stuff
local Iter = require "plenary.iterators"
local itermetatable = getmetatable(Iter:wrap())
local function forward_list_gen(param, state)
state = state + 1
local v = param[state]
if v ~= nil then
return state, v
end
end
local function backward_list_gen(param, state)
state = state - 1
local v = param[state]
if v ~= nil then
return state, v
end
end
--- Run the given predicate through all the elements pointed by this iterator,
--- and classify them into two lists. The first one holds the elements for which
--- predicate returned a truthy value, and the second holds the rest. For
--- example:
---
--- <pre>
--- local list = List{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
--- local evens, odds = list:iter():partition(function(e)
--- return e % 2 == 0
--- end)
--- print(evens, odds)
--- </pre>
---
--- Would print
---
--- <pre>
--- [0, 2, 4, 6, 8] [1, 3, 5, 7, 9]
--- </pre>
---@param predicate function: The predicate to classify the elements
---@return List,List
local function partition(self, predicate)
local list1, list2 = List.new {}, List.new {}
for _, v in self do
if predicate(v) then
list1:push(v)
else
list2:push(v)
end
end
return list1, list2
end
local function wrap_iter(f, l, n)
local iter = Iter.wrap(f, l, n)
iter.partition = partition
return iter
end
--- Counts the occurrences of e inside the list
--- @param e any: The element to test for
--- @return number: The number of occurrences of e
function List:count(e)
local count = 0
for _, v in self:iter() do
if e == v then
count = count + 1
end
end
return count
end
--- Appends the elements in the given iterator to the list
--- @param other table: An iterator object
function List:extend(other)
if type(other) == "table" and getmetatable(other) == itermetatable then
for _, v in other do
self:push(v)
end
else
error "Argument must be an iterator"
end
end
--- Checks whether there is an occurence of the given element in the list
--- @param e any: The object to test for
--- @return boolean: True if e is present
function List:contains(e)
for _, v in self:iter() do
if v == e then
return true
end
end
return false
end
--- Creates an iterator for the list. For example:
--- <pre>
--- local list = List{8, 4, 7, 9}
--- for i, v in list:iter() do
--- print(i, v)
--- end
--- </pre>
--- Would print:
--- <pre>
--- 1 8
--- 2 4
--- 3 7
--- 4 9
--- </pre>
--- @return table: An iterator object
function List:iter()
return wrap_iter(forward_list_gen, self, 0)
end
--- Creates a reverse iterator for the list. For example:
--- <pre>
--- local list = List{8, 4, 7, 9}
--- for i, v in list:riter() do
--- print(i, v)
--- end
--- </pre>
--- Would print:
--- <pre>
--- 4 9
--- 3 7
--- 2 4
--- 1 8
--- </pre>
--- @return table: An iterator object
function List:riter()
return wrap_iter(backward_list_gen, self, #self + 1)
end
-- Miscellaneous
--- Create a list from the elements pointed at by the given iterator.
--- @param iter table: An iterator object
--- @return List
function List.from_iter(iter)
local result = List.new {}
for _, v in iter do
result:push(v)
end
return result
end
return setmetatable({}, {
__call = function(_, tbl)
return List.new(tbl)
end,
__index = List,
})

View File

@ -0,0 +1,54 @@
--- I like context managers for Python
--- I want them in Lua.
local context_manager = {}
function context_manager.with(obj, callable)
-- Wrap functions for people since we're nice
if type(obj) == "function" then
obj = coroutine.create(obj)
end
if type(obj) == "thread" then
local ok, context = coroutine.resume(obj)
assert(ok, "Should have yielded in coroutine.")
local result = callable(context)
local done, _ = coroutine.resume(obj)
assert(done, "Should be done")
local no_other = not coroutine.resume(obj)
assert(no_other, "Should not yield anymore, otherwise that would make things complicated")
return result
else
assert(obj.enter)
assert(obj.exit)
-- TODO: Callable can be string for vimL function or a lua callable
local context = obj:enter()
local result = callable(context)
obj:exit()
return result
end
end
--- @param filename string|table -- If string, used as io.open(filename)
--- Else, should be a table with `filename` as an attribute
function context_manager.open(filename, mode)
if type(filename) == "table" and filename.filename then
filename = filename.filename
end
local file_io = assert(io.open(filename, mode))
return coroutine.create(function()
coroutine.yield(file_io)
file_io:close()
end)
end
return context_manager

View File

@ -0,0 +1,313 @@
--[[
Curl Wrapper
all curl methods accepts
url = "The url to make the request to.", (string)
query = "url query, append after the url", (table)
body = "The request body" (string/filepath/table)
auth = "Basic request auth, 'user:pass', or {"user", "pass"}" (string/array)
form = "request form" (table)
raw = "any additonal curl args, it must be an array/list." (array)
dry_run = "whether to return the args to be ran through curl." (boolean)
output = "where to download something." (filepath)
and returns table:
exit = "The shell process exit code." (number)
status = "The https response status." (number)
headers = "The https response headers." (array)
body = "The http response body." (string)
see test/plenary/curl_spec.lua for examples.
author = github.com/tami5
]]
--
local util, parse, request = {}, {}, nil
-- Helpers --------------------------------------------------
-------------------------------------------------------------
local F = require "plenary.functional"
local J = require "plenary.job"
local P = require "plenary.path"
-- Utils ----------------------------------------------------
-------------------------------------------------------------
util.url_encode = function(str)
if type(str) ~= "number" then
str = str:gsub("\r?\n", "\r\n")
str = str:gsub("([^%w%-%.%_%~ ])", function(c)
return string.format("%%%02X", c:byte())
end)
str = str:gsub(" ", "+")
return str
else
return str
end
end
util.kv_to_list = function(kv, prefix, sep)
return vim.tbl_flatten(F.kv_map(function(kvp)
return { prefix, kvp[1] .. sep .. kvp[2] }
end, kv))
end
util.kv_to_str = function(kv, sep, kvsep)
return F.join(
F.kv_map(function(kvp)
return kvp[1] .. kvsep .. util.url_encode(kvp[2])
end, kv),
sep
)
end
util.gen_dump_path = function()
local path
local id = string.gsub("xxxx4xxx", "[xy]", function(l)
local v = (l == "x") and math.random(0, 0xf) or math.random(0, 0xb)
return string.format("%x", v)
end)
if P.path.sep == "\\" then
path = string.format("%s\\AppData\\Local\\Temp\\plenary_curl_%s.headers", os.getenv "USERPROFILE", id)
else
path = "/tmp/plenary_curl_" .. id .. ".headers"
end
return { "-D", path }
end
-- Parsers ----------------------------------------------------
---------------------------------------------------------------
parse.headers = function(t)
if not t then
return
end
local upper = function(str)
return string.gsub(" " .. str, "%W%l", string.upper):sub(2)
end
return util.kv_to_list(
(function()
local normilzed = {}
for k, v in pairs(t) do
normilzed[upper(k:gsub("_", "%-"))] = v
end
return normilzed
end)(),
"-H",
": "
)
end
parse.data_body = function(t)
if not t then
return
end
return util.kv_to_list(t, "-d", "=")
end
parse.raw_body = function(xs)
if not xs then
return
end
if type(xs) == "table" then
return parse.data_body(xs)
else
return { "--data-raw", xs }
end
end
parse.form = function(t)
if not t then
return
end
return util.kv_to_list(t, "-F", "=")
end
parse.curl_query = function(t)
if not t then
return
end
return util.kv_to_str(t, "&", "=")
end
parse.method = function(s)
if not s then
return
end
if s ~= "head" then
return { "-X", string.upper(s) }
else
return { "-I" }
end
end
parse.file = function(p)
if not p then
return
end
return { "-d", "@" .. P.expand(P.new(p)) }
end
parse.auth = function(xs)
if not xs then
return
end
return { "-u", type(xs) == "table" and util.kv_to_str(xs, nil, ":") or xs }
end
parse.url = function(xs, q)
if not xs then
return
end
q = parse.curl_query(q)
if type(xs) == "string" then
return q and xs .. "?" .. q or xs
elseif type(xs) == "table" then
error "Low level URL definition is not supported."
end
end
parse.accept_header = function(s)
if not s then
return
end
return { "-H", "Accept: " .. s }
end
-- Parse Request -------------------------------------------
------------------------------------------------------------
parse.request = function(opts)
if opts.body then
local b = opts.body
local silent_is_file = function()
local status, result = pcall(P.is_file, P.new(b))
return status and result
end
opts.body = nil
if type(b) == "table" then
opts.data = b
elseif silent_is_file() then
opts.in_file = b
elseif type(b) == "string" then
opts.raw_body = b
end
end
local result = { "-sSL", opts.dump }
local append = function(v)
if v then
table.insert(result, v)
end
end
if opts.compressed then
table.insert(result, "--compressed")
end
append(parse.method(opts.method))
append(parse.headers(opts.headers))
append(parse.accept_header(opts.accept))
append(parse.raw_body(opts.raw_body))
append(parse.data_body(opts.data))
append(parse.form(opts.form))
append(parse.file(opts.in_file))
append(parse.auth(opts.auth))
append(opts.raw)
if opts.output then
table.insert(result, { "-o", opts.output })
end
table.insert(result, parse.url(opts.url, opts.query))
return vim.tbl_flatten(result), opts
end
-- Parse response ------------------------------------------
------------------------------------------------------------
parse.response = function(lines, dump_path, code)
local headers = P.readlines(dump_path)
local status = tonumber(string.match(headers[1], "([%w+]%d+)"))
local body = F.join(lines, "\n")
vim.loop.fs_unlink(dump_path)
table.remove(headers, 1)
return {
status = status,
headers = headers,
body = body,
exit = code,
}
end
request = function(specs)
local response = {}
local args, opts = parse.request(vim.tbl_extend("force", {
compressed = true,
dry_run = false,
dump = util.gen_dump_path(),
}, specs))
if opts.dry_run then
return args
end
local job = J:new {
command = "curl",
args = args,
on_exit = function(j, code)
if code ~= 0 then
error(
string.format(
"%s %s - curl error exit_code=%s stderr=%s",
opts.method,
opts.url,
code,
vim.inspect(j:stderr_result())
)
)
end
local output = parse.response(j:result(), opts.dump[2], code)
if opts.callback then
return opts.callback(output)
else
response = output
end
end,
}
if opts.callback then
return job:start()
else
job:sync(10000)
return response
end
end
-- Main ----------------------------------------------------
------------------------------------------------------------
return (function()
local spec = {}
local partial = function(method)
return function(url, opts)
opts = opts or {}
if type(url) == "table" then
opts = url
spec.method = method
else
spec.url = url
spec.method = method
end
opts = method == "request" and opts or (vim.tbl_extend("keep", opts, spec))
return request(opts)
end
end
return {
get = partial "get",
post = partial "post",
put = partial "put",
head = partial "head",
patch = partial "patch",
delete = partial "delete",
request = partial "request",
}
end)()

View File

@ -0,0 +1,13 @@
local debug_utils = {}
function debug_utils.sourced_filepath()
local str = debug.getinfo(2, "S").source:sub(2)
return str
end
function debug_utils.sourced_filename()
local str = debug_utils.sourced_filepath()
return str:match "^.*/(.*).lua$" or str
end
return debug_utils

View File

@ -0,0 +1,162 @@
---@brief [[
--- This module defines an idiomatic way to create enum classes, similar to
--- those in java or kotlin. There are two ways to create an enum, one is with
--- the exported `make_enum` function, or calling the module directly with the
--- enum spec.
---
--- The enum spec consists of a list-like table whose members can be either a
--- string or a tuple of the form {string, number}. In the former case, the enum
--- member will take the next available value, while in the latter, the member
--- will take the string as it's name and the number as it's value. In both
--- cases, the name must start with a capital letter.
---
--- Here is an example:
---
--- <pre>
--- local Enum = require 'plenary.enum'
--- local myEnum = Enum {
--- 'Foo', -- Takes value 1
--- 'Bar', -- Takes value 2
--- {'Qux', 10}, -- Takes value 10
--- 'Baz', -- Takes value 11
--- }
--- </pre>
---
--- In case of name or value clashing, the call will fail. For this reason, it's
--- best if you define the members in ascending order.
---@brief ]]
local Enum = {}
---@class Enum
---@class Variant
local function validate_member_name(name)
if #name > 0 and name:sub(1, 1):match "%u" then
return name
end
error('"' .. name .. '" should start with a capital letter')
end
--- Creates an enum from the given list-like table, like so:
--- <pre>
--- local enum = Enum.make_enum{
--- 'Foo',
--- 'Bar',
--- {'Qux', 10}
--- }
--- </pre>
--- @return Enum: A new enum
local function make_enum(tbl)
local enum = {}
local Variant = {}
Variant.__index = Variant
local function newVariant(i)
return setmetatable({ value = i }, Variant)
end
-- we don't need __eq because the __eq metamethod will only ever be
-- invoked when they both have the same metatable
function Variant:__lt(o)
return self.value < o.value
end
function Variant:__gt(o)
return self.value > o.value
end
function Variant:__tostring()
return tostring(self.value)
end
local function find_next_idx(e, i)
local newI = i + 1
if not e[newI] then
return newI
end
error("Overlapping index: " .. tostring(newI))
end
local i = 0
for _, v in ipairs(tbl) do
if type(v) == "string" then
local name = validate_member_name(v)
local idx = find_next_idx(enum, i)
enum[idx] = name
if enum[name] then
error("Duplicate enum member name: " .. name)
end
enum[name] = newVariant(idx)
i = idx
elseif type(v) == "table" and type(v[1]) == "string" and type(v[2]) == "number" then
local name = validate_member_name(v[1])
local idx = v[2]
if enum[idx] then
error("Overlapping index: " .. tostring(idx))
end
enum[idx] = name
if enum[name] then
error("Duplicate name: " .. name)
end
enum[name] = newVariant(idx)
i = idx
else
error "Invalid way to specify an enum variant"
end
end
return require("plenary.tbl").freeze(setmetatable(enum, Enum))
end
Enum.__index = function(_, key)
if Enum[key] then
return Enum[key]
end
error("Invalid enum key: " .. tostring(key))
end
--- Checks whether the enum has a member with the given name
--- @param key string: The element to check for
--- @return boolean: True if key is present
function Enum:has_key(key)
if rawget(getmetatable(self).__index, key) then
return true
end
return false
end
--- If there is a member named 'key', return it, otherwise return nil
--- @param key string: The element to check for
--- @return Variant: The element named by key, or nil if not present
function Enum:from_str(key)
if self:has_key(key) then
return self[key]
end
end
--- If there is a member of value 'num', return it, otherwise return nil
--- @param num number: The value of the element to check for
--- @return Variant: The element whose value is num
function Enum:from_num(num)
local key = self[num]
if key then
return self[key]
end
end
--- Checks whether the given object corresponds to an instance of Enum
--- @param tbl table: The object to be checked
--- @return boolean: True if tbl is an Enum
local function is_enum(tbl)
return getmetatable(getmetatable(tbl).__index) == Enum
end
return setmetatable({ is_enum = is_enum, make_enum = make_enum }, {
__call = function(_, tbl)
return make_enum(tbl)
end,
})

View File

@ -0,0 +1,15 @@
local M = {}
M.traceback_error = function(s, level)
local traceback = debug.traceback()
traceback = traceback .. "\n" .. s
error(traceback, (level or 1) + 1)
end
M.info_error = function(s, func_info, level)
local info = debug.getinfo(func_info)
info = info .. "\n" .. s
error(info, (level or 1) + 1)
end
return M

View File

@ -0,0 +1,190 @@
local Path = require "plenary.path"
local os_sep = Path.path.sep
local filetype = {}
local filetype_table = {
extension = {},
file_name = {},
shebang = {},
}
filetype.add_table = function(new_filetypes)
local valid_keys = { "extension", "file_name", "shebang" }
local new_keys = {}
-- Validate keys
for k, _ in pairs(new_filetypes) do
new_keys[k] = true
end
for _, k in ipairs(valid_keys) do
new_keys[k] = nil
end
for k, v in pairs(new_keys) do
error(debug.traceback("Invalid key / value:" .. tostring(k) .. " / " .. tostring(v)))
end
if new_filetypes.extension then
filetype_table.extension = vim.tbl_extend("force", filetype_table.extension, new_filetypes.extension)
end
if new_filetypes.file_name then
filetype_table.file_name = vim.tbl_extend("force", filetype_table.file_name, new_filetypes.file_name)
end
if new_filetypes.shebang then
filetype_table.shebang = vim.tbl_extend("force", filetype_table.shebang, new_filetypes.shebang)
end
end
filetype.add_file = function(filename)
local filetype_files = vim.api.nvim_get_runtime_file(string.format("data/plenary/filetypes/%s.lua", filename), true)
for _, file in ipairs(filetype_files) do
local ok, msg = pcall(filetype.add_table, dofile(file))
if not ok then
error("Unable to add file " .. file .. ":\n" .. msg)
end
end
end
local filename_regex = "[^" .. os_sep .. "].*"
filetype._get_extension_parts = function(filename)
local current_match = filename:match(filename_regex)
local possibilities = {}
while current_match do
current_match = current_match:match "[^.]%.(.*)"
if current_match then
table.insert(possibilities, current_match:lower())
else
return possibilities
end
end
return possibilities
end
filetype._parse_modeline = function(tail)
if tail:find "vim:" then
return tail:match ".*:ft=([^: ]*):.*$" or ""
end
return ""
end
filetype._parse_shebang = function(head)
if head:sub(1, 2) == "#!" then
local match = filetype_table.shebang[head:sub(3, #head)]
if match then
return match
end
end
return ""
end
local done_adding = false
local extend_tbl_with_ext_eq_ft_entries = function()
if not done_adding then
if vim.in_fast_event() then
return
end
local all_valid_filetypes = vim.fn.getcompletion("", "filetype")
for _, v in ipairs(all_valid_filetypes) do
if not filetype_table.extension[v] then
filetype_table.extension[v] = v
end
end
done_adding = true
return true
end
end
filetype.detect_from_extension = function(filepath)
local exts = filetype._get_extension_parts(filepath)
for _, ext in ipairs(exts) do
local match = ext and filetype_table.extension[ext]
if match then
return match
end
end
if extend_tbl_with_ext_eq_ft_entries() then
for _, ext in ipairs(exts) do
local match = ext and filetype_table.extension[ext]
if match then
return match
end
end
end
return ""
end
filetype.detect_from_name = function(filepath)
filepath = filepath:lower()
local split_path = vim.split(filepath, os_sep, true)
local fname = split_path[#split_path]
local match = filetype_table.file_name[fname]
if match then
return match
end
return ""
end
filetype.detect_from_modeline = function(filepath)
local tail = Path:new(filepath):readbyterange(-256, 256)
if not tail then
return ""
end
local lines = vim.split(tail, "\n")
local idx = lines[#lines] ~= "" and #lines or #lines - 1
if idx >= 1 then
return filetype._parse_modeline(lines[idx])
end
end
filetype.detect_from_shebang = function(filepath)
local head = Path:new(filepath):readbyterange(0, 256)
if not head then
return ""
end
local lines = vim.split(head, "\n")
return filetype._parse_shebang(lines[1])
end
--- Detect a filetype from a path.
---
---@param opts table: Table with optional keys
--- - fs_access (bool, default=true): Should check a file if it exists
filetype.detect = function(filepath, opts)
opts = opts or {}
opts.fs_access = opts.fs_access or true
local match = filetype.detect_from_name(filepath)
if match ~= "" then
return match
end
match = filetype.detect_from_extension(filepath)
if opts.fs_access and Path:new(filepath):exists() then
if match == "" then
match = filetype.detect_from_shebang(filepath)
if match ~= "" then
return match
end
end
if match == "text" or match == "" then
match = filetype.detect_from_modeline(filepath)
if match ~= "" then
return match
end
end
end
return match
end
filetype.add_file "base"
filetype.add_file "builtin"
return filetype

View File

@ -0,0 +1,37 @@
local tbl = require "plenary.tbl"
local M = {}
function M.bind(fn, ...)
if select("#", ...) == 1 then
local arg = ...
return function(...)
fn(arg, ...)
end
end
local args = tbl.pack(...)
return function(...)
fn(tbl.unpack(args), ...)
end
end
function M.arify(fn, argc)
return function(...)
if select("#", ...) ~= argc then
error(("Expected %s number of arguments"):format(argc))
end
fn(...)
end
end
function M.create_wrapper(map)
return function(to_wrap)
return function(...)
return map(to_wrap(...))
end
end
end
return M

View File

@ -0,0 +1,71 @@
local f = {}
function f.kv_pairs(t)
local results = {}
for k, v in pairs(t) do
table.insert(results, { k, v })
end
return results
end
function f.kv_map(fun, t)
return vim.tbl_map(fun, f.kv_pairs(t))
end
function f.join(array, sep)
return table.concat(vim.tbl_map(tostring, array), sep)
end
function f.partial(fun, ...)
local args = { ... }
return function(...)
return fun(unpack(args), ...)
end
end
function f.any(fun, iterable)
for k, v in pairs(iterable) do
if fun(k, v) then
return true
end
end
return false
end
function f.all(fun, iterable)
for k, v in pairs(iterable) do
if not fun(k, v) then
return false
end
end
return true
end
function f.if_nil(val, was_nil, was_not_nil)
if val == nil then
return was_nil
else
return was_not_nil
end
end
function f.select_only(n)
return function(...)
local x = select(n, ...)
return x
end
end
f.first = f.select_only(1)
f.second = f.select_only(2)
f.third = f.select_only(3)
function f.last(...)
local length = select("#", ...)
local x = select(length, ...)
return x
end
return f

View File

@ -0,0 +1,20 @@
PLENARY_DEBUG = PLENARY_DEBUG == nil and true or PLENARY_DEBUG
if PLENARY_DEBUG then
require("plenary.reload").reload_module "plenary"
end
-- Lazy load everything into plenary.
local plenary = setmetatable({}, {
__index = function(t, k)
local ok, val = pcall(require, string.format("plenary.%s", k))
if ok then
rawset(t, k, val)
end
return val
end,
})
return plenary

View File

@ -0,0 +1,670 @@
---@brief [[
---An adaptation of luafun for neovim.
---This library will use neovim specific functions.
---Some documentation is the same as from luafun.
---Some extra functions are present that are not in luafun
---@brief ]]
local co = coroutine
local f = require "plenary.functional"
--------------------------------------------------------------------------------
-- Tools
--------------------------------------------------------------------------------
local exports = {}
---@class Iterator
---@field gen function
---@field param any
---@field state any
local Iterator = {}
Iterator.__index = Iterator
---Makes a for loop work
---If not called without param or state, will just generate with the starting state
---This is useful because the original luafun will also return param and state in addition to the iterator as a multival
---This can cause problems because when using iterators as expressions the multivals can bleed
---For example i.iter { 1, 2, i.iter { 3, 4 } } will not work because the inner iterator returns a multival thus polluting the list with internal values
---So instead we do not return param and state as multivals when doing wrap
---This causes the first loop iteration to call param and state with nil because we didn't return them as multivals
---We have to use or to check for nil and default to interal starting state and param
function Iterator:__call(param, state)
return self.gen(param or self.param, state or self.state)
end
function Iterator:__tostring()
return "<iterator>"
end
-- A special hack for zip/chain to skip last two state, if a wrapped iterator
-- has been passed
local numargs = function(...)
local n = select("#", ...)
if n >= 3 then
-- Fix last argument
local it = select(n - 2, ...)
if
type(it) == "table"
and getmetatable(it) == Iterator
and it.param == select(n - 1, ...)
and it.state == select(n, ...)
then
return n - 2
end
end
return n
end
local return_if_not_empty = function(state_x, ...)
if state_x == nil then
return nil
end
return ...
end
local call_if_not_empty = function(fun, state_x, ...)
if state_x == nil then
return nil
end
return state_x, fun(...)
end
--------------------------------------------------------------------------------
-- Basic Functions
--------------------------------------------------------------------------------
local nil_gen = function(_param, _state)
return nil
end
local ipairs_gen = ipairs {}
local pairs_gen = pairs {}
local map_gen = function(map, key)
key, value = pairs_gen(map, key)
return key, key, value
end
local string_gen = function(param, state)
state = state + 1
if state > #param then
return nil
end
local r = string.sub(param, state, state)
return state, r
end
local rawiter = function(obj, param, state)
assert(obj ~= nil, "invalid iterator")
if type(obj) == "table" then
local mt = getmetatable(obj)
if mt ~= nil then
if mt == Iterator then
return obj.gen, obj.param, obj.state
end
end
if vim.tbl_islist(obj) then
return ipairs(obj)
else
-- hash
return map_gen, obj, nil
end
elseif type(obj) == "function" then
return obj, param, state
elseif type(obj) == "string" then
if #obj == 0 then
return nil_gen, nil, nil
end
return string_gen, obj, 0
end
error(string.format('object %s of type "%s" is not iterable', obj, type(obj)))
end
---Wraps the iterator triplet into a table to allow metamethods and calling with method form
---Important! We do not return param and state as multivals like the original luafun
---Se the __call metamethod for more information
---@param gen any
---@param param any
---@param state any
---@return Iterator
local function wrap(gen, param, state)
return setmetatable({
gen = gen,
param = param,
state = state,
}, Iterator)
end
---Unwrap an iterator metatable into the iterator triplet
---@param self Iterator
---@return any
---@return any
---@return any
local unwrap = function(self)
return self.gen, self.param, self.state
end
---Create an iterator from an object
---@param obj any
---@param param any (optional)
---@param state any (optional)
---@return Iterator
local iter = function(obj, param, state)
return wrap(rawiter(obj, param, state))
end
exports.iter = iter
exports.wrap = wrap
exports.unwrap = unwrap
function Iterator:for_each(fn)
local param, state = self.param, self.state
repeat
state = call_if_not_empty(fn, self.gen(param, state))
until state == nil
end
function Iterator:stateful()
return wrap(
co.wrap(function()
self:for_each(function(...)
co.yield(f.first(...), ...)
end)
-- too make sure that we always return nil if there are no more
while true do
co.yield()
end
end),
nil,
nil
)
end
-- function Iterator:stateful()
-- local gen, param, state = self.gen, self.param, self.state
-- local function return_and_set_state(state_x, ...)
-- state = state_x
-- if state == nil then return end
-- return state_x, ...
-- end
-- local stateful_gen = function()
-- return return_and_set_state(gen(param, state))
-- end
-- return wrap(stateful_gen, false, false)
-- end
--------------------------------------------------------------------------------
-- Generators
--------------------------------------------------------------------------------
local range_gen = function(param, state)
local stop, step = param[1], param[2]
state = state + step
if state > stop then
return nil
end
return state, state
end
local range_rev_gen = function(param, state)
local stop, step = param[1], param[2]
state = state + step
if state < stop then
return nil
end
return state, state
end
---Creates a range iterator
---@param start number
---@param stop number
---@param step number
---@return Iterator
local range = function(start, stop, step)
if step == nil then
if stop == nil then
if start == 0 then
return nil_gen, nil, nil
end
stop = start
start = stop > 0 and 1 or -1
end
step = start <= stop and 1 or -1
end
assert(type(start) == "number", "start must be a number")
assert(type(stop) == "number", "stop must be a number")
assert(type(step) == "number", "step must be a number")
assert(step ~= 0, "step must not be zero")
if step > 0 then
return wrap(range_gen, { stop, step }, start - step)
elseif step < 0 then
return wrap(range_rev_gen, { stop, step }, start - step)
end
end
exports.range = range
local duplicate_table_gen = function(param_x, state_x)
return state_x + 1, unpack(param_x)
end
local duplicate_fun_gen = function(param_x, state_x)
return state_x + 1, param_x(state_x)
end
local duplicate_gen = function(param_x, state_x)
return state_x + 1, param_x
end
---Creates an infinite iterator that will yield the arguments
---If multiple arguments are passed, the args will be packed and unpacked
---@param ...: the arguments to duplicate
---@return Iterator
local duplicate = function(...)
if select("#", ...) <= 1 then
return wrap(duplicate_gen, select(1, ...), 0)
else
return wrap(duplicate_table_gen, { ... }, 0)
end
end
exports.duplicate = duplicate
---Creates an iterator from a function
---NOTE: if the function is a closure and modifies state, the resulting iterator will not be stateless
---@param fun function
---@return Iterator
local from_fun = function(fun)
assert(type(fun) == "function")
return wrap(duplicate_fun_gen, fun, 0)
end
exports.from_fun = from_fun
---Creates an infinite iterator that will yield zeros.
---This is an alias to calling duplicate(0)
---@return Iterator
local zeros = function()
return wrap(duplicate_gen, 0, 0)
end
exports.zeros = zeros
---Creates an infinite iterator that will yield ones.
---This is an alias to calling duplicate(1)
---@return Iterator
local ones = function()
return wrap(duplicate_gen, 1, 0)
end
exports.ones = ones
local rands_gen = function(param_x, _state_x)
return 0, math.random(param_x[1], param_x[2])
end
local rands_nil_gen = function(_param_x, _state_x)
return 0, math.random()
end
---Creates an infinite iterator that will yield random values.
---@param n number
---@param m number
---@return Iterator
local rands = function(n, m)
if n == nil and m == nil then
return wrap(rands_nil_gen, 0, 0)
end
assert(type(n) == "number", "invalid first arg to rands")
if m == nil then
m = n
n = 0
else
assert(type(m) == "number", "invalid second arg to rands")
end
assert(n < m, "empty interval")
return wrap(rands_gen, { n, m - 1 }, 0)
end
exports.rands = rands
local split_gen = function(param, state)
local input, sep = param[1], param[2]
local input_len = #input
if state > input_len + 1 then
return
end
local start, finish = string.find(input, sep, state, true)
if not start then
start = input_len + 1
finish = input_len + 1
end
local sub_str = input:sub(state, start - 1)
return finish + 1, sub_str
end
---Return an iterator of substrings separated by a string
---@param input string: the string to split
---@param sep string: the separator to find and split based on
---@return Iterator
local split = function(input, sep)
return wrap(split_gen, { input, sep }, 1)
end
exports.split = split
---Splits a string based on a single space
---An alias for split(input, " ")
---@param input any
---@return any
local words = function(input)
return split(input, " ")
end
exports.words = words
local lines = function(input)
-- TODO: platform specific linebreaks
return split(input, "\n")
end
exports.lines = lines
--------------------------------------------------------------------------------
-- Transformations
--------------------------------------------------------------------------------
local map_gen = function(param, state)
local gen_x, param_x, fun = param[1], param[2], param[3]
return call_if_not_empty(fun, gen_x(param_x, state))
end
---Iterator adapter that maps the previous iterator with a function
---@param fun function: The function to map with. Will be called on each element
---@return Iterator
function Iterator:map(fun)
return wrap(map_gen, { self.gen, self.param, fun }, self.state)
end
local flatten_gen1
do
local it = function(new_iter, state_x, ...)
if state_x == nil then
return nil
end
return { new_iter.gen, new_iter.param, state_x }, ...
end
flatten_gen1 = function(state, state_x, ...)
if state_x == nil then
return nil
end
local first_arg = f.first(...)
-- experimental part
if getmetatable(first_arg) == Iterator then
-- attach the iterator to the rest
local new_iter = (first_arg .. wrap(state[1], state[2], state_x)):flatten()
-- advance the iterator by one
return it(new_iter, new_iter.gen(new_iter.param, new_iter.state))
end
return { state[1], state[2], state_x }, ...
end
end
local flatten_gen = function(_, state)
if state == nil then
return
end
local gen_x, param_x, state_x = state[1], state[2], state[3]
return flatten_gen1(state, gen_x(param_x, state_x))
end
---Iterator adapter that will recursivley flatten nested iterator structure
---@return Iterator
function Iterator:flatten()
return wrap(flatten_gen, false, { self.gen, self.param, self.state })
end
--------------------------------------------------------------------------------
-- Filtering
--------------------------------------------------------------------------------
local filter1_gen = function(fun, gen_x, param_x, state_x, a)
while true do
if state_x == nil or fun(a) then
break
end
state_x, a = gen_x(param_x, state_x)
end
return state_x, a
end
-- call each other
-- because we can't assign a vararg mutably in a while loop like filter1_gen
-- so we have to use recursion in calling both of these functions
local filterm_gen
local filterm_gen_shrink = function(fun, gen_x, param_x, state_x)
return filterm_gen(fun, gen_x, param_x, gen_x(param_x, state_x))
end
filterm_gen = function(fun, gen_x, param_x, state_x, ...)
if state_x == nil then
return nil
end
if fun(...) then
return state_x, ...
end
return filterm_gen_shrink(fun, gen_x, param_x, state_x)
end
local filter_detect = function(fun, gen_x, param_x, state_x, ...)
if select("#", ...) < 2 then
return filter1_gen(fun, gen_x, param_x, state_x, ...)
else
return filterm_gen(fun, gen_x, param_x, state_x, ...)
end
end
local filter_gen = function(param, state_x)
local gen_x, param_x, fun = param[1], param[2], param[3]
return filter_detect(fun, gen_x, param_x, gen_x(param_x, state_x))
end
---Iterator adapter that will filter values
---@param fun function: The function to filter values with. If the function returns true, the value will be kept.
---@return Iterator
function Iterator:filter(fun)
return wrap(filter_gen, { self.gen, self.param, fun }, self.state)
end
--------------------------------------------------------------------------------
-- Reducing
--------------------------------------------------------------------------------
---Returns true if any of the values in the iterator satisfy a predicate
---@param fun function
---@return boolean
function Iterator:any(fun)
local r
local state, param, gen = self.state, self.param, self.gen
repeat
state, r = call_if_not_empty(fun, gen(param, state))
until state == nil or r
return r
end
---Returns true if all of the values in the iterator satisfy a predicate
---@param fun function
---@return boolean
function Iterator:all(fun)
local r
local state, param, gen = self.state, self.param, self.gen
repeat
state, r = call_if_not_empty(fun, gen(param, state))
until state == nil or not r
return state == nil
end
---Finds a value that is equal to the provided value of satisfies a predicate.
---@param val_or_fn any
---@return any
function Iterator:find(val_or_fn)
local gen, param, state = self.gen, self.param, self.state
if type(val_or_fn) == "function" then
return return_if_not_empty(filter_detect(val_or_fn, gen, param, gen(param, state)))
else
for _, r in gen, param, state do
if r == val_or_fn then
return r
end
end
return nil
end
end
---Turns an iterator into a list.
---If the iterator yields multivals only the first multival will be used.
---@return table
function Iterator:tolist()
local list = {}
self:for_each(function(a)
table.insert(list, a)
end)
return list
end
---Turns an iterator into a list.
---If the iterator yields multivals all multivals will be used and packed into a table.
---@return table
function Iterator:tolistn()
local list = {}
self:for_each(function(...)
table.insert(list, { ... })
end)
return list
end
---Turns an iterator into a map.
---The first multival that the iterator yields will be the key.
---The second multival that the iterator yields will be the value.
---@return table
function Iterator:tomap()
local map = {}
self:for_each(function(key, value)
map[key] = value
end)
return map
end
--------------------------------------------------------------------------------
-- Compositions
--------------------------------------------------------------------------------
-- call each other
local chain_gen_r1
local chain_gen_r2 = function(param, state, state_x, ...)
if state_x == nil then
local i = state[1] + 1
if param[3 * i - 1] == nil then
return nil
end
state_x = param[3 * i]
return chain_gen_r1(param, { i, state_x })
end
return { state[1], state_x }, ...
end
chain_gen_r1 = function(param, state)
local i, state_x = state[1], state[2]
local gen_x, param_x = param[3 * i - 2], param[3 * i - 1]
return chain_gen_r2(param, state, gen_x(param_x, state_x))
end
---Make an iterator that returns elements from the first iterator until it is exhausted, then proceeds to the next iterator,
---until all of the iterators are exhausted.
---Used for treating consecutive iterators as a single iterator.
---Infinity iterators are supported, but are not recommended.
---@param ...: the iterators to chain
---@return Iterator
local chain = function(...)
local n = numargs(...)
if n == 0 then
return wrap(nil_gen, nil, nil)
end
local param = { [3 * n] = 0 }
local i, gen_x, param_x, state_x
for i = 1, n, 1 do
local elem = select(i, ...)
gen_x, param_x, state_x = unwrap(elem)
param[3 * i - 2] = gen_x
param[3 * i - 1] = param_x
param[3 * i] = state_x
end
return wrap(chain_gen_r1, param, { 1, param[3] })
end
Iterator.chain = chain
Iterator.__concat = chain
exports.chain = chain
local function zip_gen_r(param, state, state_new, ...)
if #state_new == #param / 2 then
return state_new, ...
end
local i = #state_new + 1
local gen_x, param_x = param[2 * i - 1], param[2 * i]
local state_x, r = gen_x(param_x, state[i])
if state_x == nil then
return nil
end
table.insert(state_new, state_x)
return zip_gen_r(param, state, state_new, r, ...)
end
local zip_gen = function(param, state)
return zip_gen_r(param, state, {})
end
---Return a new iterator where i-th return value contains the i-th element from each of the iterators.
---The returned iterator is truncated in length to the length of the shortest iterator.
---For multi-return iterators only the first variable is used.
---@param ...: the iterators to zip
---@return Iterator
local zip = function(...)
local n = numargs(...)
if n == 0 then
return wrap(nil_gen, nil, nil)
end
local param = { [2 * n] = 0 }
local state = { [n] = 0 }
local i, gen_x, param_x, state_x
for i = 1, n, 1 do
local it = select(n - i + 1, ...)
gen_x, param_x, state_x = rawiter(it)
param[2 * i - 1] = gen_x
param[2 * i] = param_x
state[i] = state_x
end
return wrap(zip_gen, param, state)
end
Iterator.zip = zip
Iterator.__div = zip
exports.zip = zip
return exports

View File

@ -0,0 +1,680 @@
local vim = vim
local uv = vim.loop
local F = require "plenary.functional"
---@class Job
---@field command string : Command to run
---@field args Array : List of arguments to pass
---@field cwd string : Working directory for job
---@field env Map|Array : Environment looking like: { ['VAR'] = 'VALUE } or { 'VAR=VALUE' }
---@field skip_validation boolean : Skip validating the arguments
---@field enable_handlers boolean : If set to false, disables all callbacks associated with output
---@field on_start function : Run when starting job
---@field on_stdout function : (error: string, data: string, self? Job)
---@field on_stderr function : (error: string, data: string, self? Job)
---@field on_exit function : (self, code: number, signal: number)
---@field maximum_results number : stop processing results after this number
---@field writer Job|table|string : Job that writes to stdin of this job.
---@field detached boolean : Spawn the child in a detached state making it a process group leader
---@field enabled_recording boolean
local Job = {}
Job.__index = Job
local function close_safely(j, key)
local handle = j[key]
if not handle then
return
end
if not handle:is_closing() then
handle:close()
end
end
local start_shutdown_check = function(child, options, code, signal)
uv.check_start(child._shutdown_check, function()
if not child:_pipes_are_closed(options) then
return
end
-- Wait until all the pipes are closing.
uv.check_stop(child._shutdown_check)
child._shutdown_check = nil
child:_shutdown(code, signal)
-- Remove left over references
child = nil
end)
end
local shutdown_factory = function(child, options)
return function(code, signal)
if uv.is_closing(child._shutdown_check) then
return child:shutdown(code, signal)
else
start_shutdown_check(child, options, code, signal)
end
end
end
local function expand(path)
if vim.in_fast_event() then
return assert(uv.fs_realpath(path), string.format("Path must be valid: %s", path))
else
-- TODO: Probably want to check that this is valid here... otherwise that's weird.
return vim.fn.expand(path, true)
end
end
---@class Array
--- Numeric table
---@class Map
--- Map-like table
---Create a new job
---@param o Job
---@return Job
function Job:new(o)
if not o then
error(debug.traceback "Options are required for Job:new")
end
local command = o.command
if not command then
if o[1] then
command = o[1]
else
error(debug.traceback "'command' is required for Job:new")
end
elseif o[1] then
error(debug.traceback "Cannot pass both 'command' and array args")
end
local args = o.args
if not args then
if #o > 1 then
args = { select(2, unpack(o)) }
end
end
local ok, is_exe = pcall(vim.fn.executable, command)
if not o.skip_validation and ok and 1 ~= is_exe then
error(debug.traceback(command .. ": Executable not found"))
end
local obj = {}
obj.command = command
obj.args = args
obj._raw_cwd = o.cwd
if o.env then
if type(o.env) ~= "table" then
error "[plenary.job] env has to be a table"
end
local transform = {}
for k, v in pairs(o.env) do
if type(k) == "number" then
table.insert(transform, v)
elseif type(k) == "string" then
table.insert(transform, k .. "=" .. tostring(v))
end
end
obj.env = transform
end
if o.interactive == nil then
obj.interactive = true
else
obj.interactive = o.interactive
end
if o.detached then
obj.detached = true
end
-- enable_handlers: Do you want to do ANYTHING with the stdout/stderr of the proc
obj.enable_handlers = F.if_nil(o.enable_handlers, true, o.enable_handlers)
-- enable_recording: Do you want to record stdout/stderr into a table.
-- Since it cannot be enabled when enable_handlers is false,
-- we try and make sure they are associated correctly.
obj.enable_recording = F.if_nil(
F.if_nil(o.enable_recording, o.enable_handlers, o.enable_recording),
true,
o.enable_recording
)
if not obj.enable_handlers and obj.enable_recording then
error "[plenary.job] Cannot record items but disable handlers"
end
obj._user_on_start = o.on_start
obj._user_on_stdout = o.on_stdout
obj._user_on_stderr = o.on_stderr
obj._user_on_exit = o.on_exit
obj._additional_on_exit_callbacks = {}
obj._maximum_results = o.maximum_results
obj.user_data = {}
obj.writer = o.writer
self._reset(obj)
return setmetatable(obj, self)
end
function Job:_reset()
self.is_shutdown = nil
if self._shutdown_check and uv.is_active(self._shutdown_check) and not uv.is_closing(self._shutdown_check) then
vim.api.nvim_err_writeln(debug.traceback "We may be memory leaking here. Please report to TJ.")
end
self._shutdown_check = uv.new_check()
self.stdin = nil
self.stdout = nil
self.stderr = nil
self._stdout_reader = nil
self._stderr_reader = nil
if self.enable_recording then
self._stdout_results = {}
self._stderr_results = {}
else
self._stdout_results = nil
self._stderr_results = nil
end
end
--- Stop a job and close all handles
function Job:_stop()
close_safely(self, "stdin")
close_safely(self, "stderr")
close_safely(self, "stdout")
close_safely(self, "handle")
end
function Job:_pipes_are_closed(options)
for _, pipe in ipairs { options.stdin, options.stdout, options.stderr } do
if pipe and not uv.is_closing(pipe) then
return false
end
end
return true
end
--- Shutdown a job.
function Job:shutdown(code, signal)
if self._shutdown_check and not uv.is_active(self._shutdown_check) then
vim.wait(1000, function()
return self:_pipes_are_closed(self) and self.is_shutdown
end, 1, true)
end
self:_shutdown(code, signal)
end
function Job:_shutdown(code, signal)
if self.is_shutdown then
return
end
self.code = code
self.signal = signal
if self._stdout_reader then
pcall(self._stdout_reader, nil, nil, true)
end
if self._stderr_reader then
pcall(self._stderr_reader, nil, nil, true)
end
if self._user_on_exit then
self:_user_on_exit(code, signal)
end
for _, v in ipairs(self._additional_on_exit_callbacks) do
v(self, code, signal)
end
if self.stdout then
self.stdout:read_stop()
end
if self.stderr then
self.stderr:read_stop()
end
self:_stop()
self.is_shutdown = true
self._stdout_reader = nil
self._stderr_reader = nil
end
function Job:_create_uv_options()
local options = {}
options.command = self.command
options.args = self.args
options.stdio = { self.stdin, self.stdout, self.stderr }
if self._raw_cwd then
options.cwd = expand(self._raw_cwd)
end
if self.env then
options.env = self.env
end
if self.detached then
options.detached = true
end
return options
end
local on_output = function(self, result_key, cb)
return coroutine.wrap(function(err, data, is_complete)
local result_index = 1
local line, start, result_line, found_newline
-- We repeat forever as a coroutine so that we can keep calling this.
while true do
if data then
data = data:gsub("\r", "")
local processed_index = 1
local data_length = #data + 1
repeat
start = string.find(data, "\n", processed_index, true) or data_length
line = string.sub(data, processed_index, start - 1)
found_newline = start ~= data_length
-- Concat to last line if there was something there already.
-- This happens when "data" is broken into chunks and sometimes
-- the content is sent without any newlines.
if result_line then
-- results[result_index] = results[result_index] .. line
result_line = result_line .. line
-- Only put in a new line when we actually have new data to split.
-- This is generally only false when we do end with a new line.
-- It prevents putting in a "" to the end of the results.
elseif start ~= processed_index or found_newline then
-- results[result_index] = line
result_line = line
-- Otherwise, we don't need to do anything.
end
if found_newline then
if not result_line then
return vim.api.nvim_err_writeln(
"Broken data thing due to: " .. tostring(result_line) .. " " .. tostring(data)
)
end
if self.enable_recording then
self[result_key][result_index] = result_line
end
if cb then
cb(err, result_line, self)
end
-- Stop processing if we've surpassed the maximum.
if self._maximum_results and result_index > self._maximum_results then
-- Shutdown once we get the chance.
-- Can't call it here, because we'll just keep calling ourselves.
vim.schedule(function()
self:shutdown()
end)
return
end
result_index = result_index + 1
result_line = nil
end
processed_index = start + 1
until not found_newline
end
if self.enable_recording then
self[result_key][result_index] = result_line
end
-- If we didn't get a newline on the last execute, send the final results.
if cb and is_complete and not found_newline then
cb(err, result_line, self)
end
if data == nil or is_complete then
return
end
err, data, is_complete = coroutine.yield()
end
end)
end
--- Stop previous execution and add new pipes.
--- Also regenerates pipes of writer.
function Job:_prepare_pipes()
self:_stop()
if self.writer then
if Job.is_job(self.writer) then
self.writer:_prepare_pipes()
self.stdin = self.writer.stdout
elseif self.writer.write then
self.stdin = self.writer
end
end
if not self.stdin then
self.stdin = self.interactive and uv.new_pipe(false) or nil
end
self.stdout = uv.new_pipe(false)
self.stderr = uv.new_pipe(false)
end
--- Execute job. Should be called only after preprocessing is done.
function Job:_execute()
local options = self:_create_uv_options()
if self._user_on_start then
self:_user_on_start()
end
self.handle, self.pid = uv.spawn(options.command, options, shutdown_factory(self, options))
if not self.handle then
error(debug.traceback("Failed to spawn process: " .. vim.inspect(self)))
end
if self.enable_handlers then
self._stdout_reader = on_output(self, "_stdout_results", self._user_on_stdout)
self.stdout:read_start(self._stdout_reader)
self._stderr_reader = on_output(self, "_stderr_results", self._user_on_stderr)
self.stderr:read_start(self._stderr_reader)
end
if self.writer then
if Job.is_job(self.writer) then
self.writer:_execute()
elseif type(self.writer) == "table" and vim.tbl_islist(self.writer) then
local writer_len = #self.writer
for i, v in ipairs(self.writer) do
self.stdin:write(v)
if i ~= writer_len then
self.stdin:write "\n"
else
self.stdin:write("\n", function()
pcall(self.stdin.close, self.stdin)
end)
end
end
elseif type(self.writer) == "string" then
self.stdin:write(self.writer, function()
self.stdin:close()
end)
elseif self.writer.write then
self.stdin = self.writer
else
error("Unknown self.writer: " .. vim.inspect(self.writer))
end
end
return self
end
function Job:start()
self:_reset()
self:_prepare_pipes()
self:_execute()
end
function Job:sync(timeout, wait_interval)
self:start()
self:wait(timeout, wait_interval)
return self.enable_recording and self:result() or nil, self.code
end
function Job:result()
assert(self.enable_recording, "'enabled_recording' is not enabled for this job.")
return self._stdout_results
end
function Job:stderr_result()
assert(self.enable_recording, "'enabled_recording' is not enabled for this job.")
return self._stderr_results
end
function Job:pid()
return self.pid
end
function Job:wait(timeout, wait_interval, should_redraw)
timeout = timeout or 5000
wait_interval = wait_interval or 10
if self.handle == nil then
local msg = vim.inspect(self)
vim.schedule(function()
vim.api.nvim_err_writeln(msg)
end)
return
end
-- Wait five seconds, or until timeout.
local wait_result = vim.wait(timeout, function()
if should_redraw then
vim.cmd [[redraw!]]
end
if self.is_shutdown then
assert(not self.handle or self.handle:is_closing(), "Job must be shutdown if it's closing")
end
return self.is_shutdown
end, wait_interval, not should_redraw)
if not wait_result then
error(
string.format(
"'%s %s' was unable to complete in %s ms",
self.command,
table.concat(self.args or {}, " "),
timeout
)
)
end
return self
end
function Job:co_wait(wait_time)
wait_time = wait_time or 5
if self.handle == nil then
vim.api.nvim_err_writeln(vim.inspect(self))
return
end
while not vim.wait(wait_time, function()
return self.is_shutdown
end) do
coroutine.yield()
end
return self
end
--- Wait for all jobs to complete
function Job.join(...)
local jobs_to_wait = { ... }
local num_jobs = table.getn(jobs_to_wait)
-- last entry can be timeout
local timeout
if type(jobs_to_wait[num_jobs]) == "number" then
timeout = table.remove(jobs_to_wait, num_jobs)
num_jobs = num_jobs - 1
end
local completed = 0
return vim.wait(timeout or 10000, function()
for index, current_job in pairs(jobs_to_wait) do
if current_job.is_shutdown then
jobs_to_wait[index] = nil
completed = completed + 1
end
end
return num_jobs == completed
end)
end
local _request_id = 0
local _request_status = {}
function Job:and_then(next_job)
self:add_on_exit_callback(function()
next_job:start()
end)
end
function Job:and_then_wrap(next_job)
self:add_on_exit_callback(vim.schedule_wrap(function()
next_job:start()
end))
end
function Job:after(fn)
self:add_on_exit_callback(fn)
return self
end
function Job:and_then_on_success(next_job)
self:add_on_exit_callback(function(_, code)
if code == 0 then
next_job:start()
end
end)
end
function Job:and_then_on_success_wrap(next_job)
self:add_on_exit_callback(vim.schedule_wrap(function(_, code)
if code == 0 then
next_job:start()
end
end))
end
function Job:after_success(fn)
self:add_on_exit_callback(function(j, code, signal)
if code == 0 then
fn(j, code, signal)
end
end)
end
function Job:and_then_on_failure(next_job)
self:add_on_exit_callback(function(_, code)
if code ~= 0 then
next_job:start()
end
end)
end
function Job:and_then_on_failure_wrap(next_job)
self:add_on_exit_callback(vim.schedule_wrap(function(_, code)
if code ~= 0 then
next_job:start()
end
end))
end
function Job:after_failure(fn)
self:add_on_exit_callback(function(j, code, signal)
if code ~= 0 then
fn(j, code, signal)
end
end)
end
function Job.chain(...)
_request_id = _request_id + 1
_request_status[_request_id] = false
local jobs = { ... }
for index = 2, #jobs do
local prev_job = jobs[index - 1]
local job = jobs[index]
prev_job:add_on_exit_callback(vim.schedule_wrap(function()
job:start()
end))
end
local last_on_exit = jobs[#jobs]._user_on_exit
jobs[#jobs]._user_on_exit = function(self, err, data)
if last_on_exit then
last_on_exit(self, err, data)
end
_request_status[_request_id] = true
end
jobs[1]:start()
return _request_id
end
function Job.chain_status(id)
return _request_status[id]
end
function Job.is_job(item)
if type(item) ~= "table" then
return false
end
return getmetatable(item) == Job
end
function Job:add_on_exit_callback(cb)
table.insert(self._additional_on_exit_callbacks, cb)
end
--- Send data to a job.
function Job:send(data)
if not self.stdin then
error "job has no 'stdin'. Have you run `job:start()` yet?"
end
self.stdin:write(data)
end
return Job

View File

@ -0,0 +1,102 @@
-- based on https://github.com/sindresorhus/strip-json-comments
local singleComment = "singleComment"
local multiComment = "multiComment"
local stripWithoutWhitespace = function()
return ""
end
local function slice(str, from, to)
from = from or 1
to = to or #str
return str:sub(from, to)
end
local stripWithWhitespace = function(str, from, to)
return slice(str, from, to):gsub("%S", " ")
end
local isEscaped = function(jsonString, quotePosition)
local index = quotePosition - 1
local backslashCount = 0
while jsonString:sub(index, index) == "\\" do
index = index - 1
backslashCount = backslashCount + 1
end
return backslashCount % 2 == 1 and true or false
end
local M = {}
-- Strips any json comments from a json string.
-- The resulting string can then be used by `vim.fn.json_decode`
--
---@param jsonString string
---@param options table
--- * whitespace:
--- - defaults to true
--- - when true, comments will be replaced by whitespace
--- - when false, comments will be stripped
function M.json_strip_comments(jsonString, options)
options = options or {}
local strip = options.whitespace == false and stripWithoutWhitespace or stripWithWhitespace
local insideString = false
local insideComment = false
local offset = 1
local result = ""
local skip = false
for i = 1, #jsonString, 1 do
if skip then
skip = false
else
local currentCharacter = jsonString:sub(i, i)
local nextCharacter = jsonString:sub(i + 1, i + 1)
if not insideComment and currentCharacter == '"' then
local escaped = isEscaped(jsonString, i)
if not escaped then
insideString = not insideString
end
end
if not insideString then
if not insideComment and currentCharacter .. nextCharacter == "//" then
result = result .. slice(jsonString, offset, i - 1)
offset = i
insideComment = singleComment
i = i + 1
skip = true
elseif insideComment == singleComment and currentCharacter .. nextCharacter == "\r\n" then
i = i + 1
skip = true
insideComment = false
result = result .. strip(jsonString, offset, i - 1)
offset = i
elseif insideComment == singleComment and currentCharacter == "\n" then
insideComment = false
result = result .. strip(jsonString, offset, i - 1)
offset = i
elseif not insideComment and currentCharacter .. nextCharacter == "/*" then
result = result .. slice(jsonString, offset, i - 1)
offset = i
insideComment = multiComment
i = i + 1
skip = true
elseif insideComment == multiComment and currentCharacter .. nextCharacter == "*/" then
i = i + 1
skip = true
insideComment = false
result = result .. strip(jsonString, offset, i)
offset = i + 1
end
end
end
end
return result .. (insideComment and strip(slice(jsonString, offset)) or slice(jsonString, offset))
end
return M

View File

@ -0,0 +1,201 @@
-- log.lua
--
-- Inspired by rxi/log.lua
-- Modified by tjdevries and can be found at github.com/tjdevries/vlog.nvim
--
-- This library is free software; you can redistribute it and/or modify it
-- under the terms of the MIT license. See LICENSE for details.
local p_debug = vim.fn.getenv "DEBUG_PLENARY"
if p_debug == vim.NIL then
p_debug = false
end
-- User configuration section
local default_config = {
-- Name of the plugin. Prepended to log messages
plugin = "plenary",
-- Should print the output to neovim while running
-- values: 'sync','async',false
use_console = "async",
-- Should highlighting be used in console (using echohl)
highlights = true,
-- Should write to a file
use_file = true,
-- Should write to the quickfix list
use_quickfix = false,
-- Any messages above this level will be logged.
level = p_debug and "debug" or "info",
-- Level configuration
modes = {
{ name = "trace", hl = "Comment" },
{ name = "debug", hl = "Comment" },
{ name = "info", hl = "None" },
{ name = "warn", hl = "WarningMsg" },
{ name = "error", hl = "ErrorMsg" },
{ name = "fatal", hl = "ErrorMsg" },
},
-- Can limit the number of decimals displayed for floats
float_precision = 0.01,
}
-- {{{ NO NEED TO CHANGE
local log = {}
local unpack = unpack or table.unpack
log.new = function(config, standalone)
config = vim.tbl_deep_extend("force", default_config, config)
local outfile = string.format("%s/%s.log", vim.api.nvim_call_function("stdpath", { "cache" }), config.plugin)
local obj
if standalone then
obj = log
else
obj = config
end
local levels = {}
for i, v in ipairs(config.modes) do
levels[v.name] = i
end
local round = function(x, increment)
increment = increment or 1
x = x / increment
return (x > 0 and math.floor(x + 0.5) or math.ceil(x - 0.5)) * increment
end
local make_string = function(...)
local t = {}
for i = 1, select("#", ...) do
local x = select(i, ...)
if type(x) == "number" and config.float_precision then
x = tostring(round(x, config.float_precision))
elseif type(x) == "table" then
x = vim.inspect(x)
else
x = tostring(x)
end
t[#t + 1] = x
end
return table.concat(t, " ")
end
local log_at_level = function(level, level_config, message_maker, ...)
-- Return early if we're below the config.level
if level < levels[config.level] then
return
end
local nameupper = level_config.name:upper()
local msg = message_maker(...)
local info = debug.getinfo(config.info_level or 2, "Sl")
local lineinfo = info.short_src .. ":" .. info.currentline
-- Output to console
if config.use_console then
local log_to_console = function()
local console_string = string.format("[%-6s%s] %s: %s", nameupper, os.date "%H:%M:%S", lineinfo, msg)
if config.highlights and level_config.hl then
vim.cmd(string.format("echohl %s", level_config.hl))
end
local split_console = vim.split(console_string, "\n")
for _, v in ipairs(split_console) do
local formatted_msg = string.format("[%s] %s", config.plugin, vim.fn.escape(v, [["\]]))
local ok = pcall(vim.cmd, string.format([[echom "%s"]], formatted_msg))
if not ok then
vim.api.nvim_out_write(msg .. "\n")
end
end
if config.highlights and level_config.hl then
vim.cmd "echohl NONE"
end
end
if config.use_console == "sync" and not vim.in_fast_event() then
log_to_console()
else
vim.schedule(log_to_console)
end
end
-- Output to log file
if config.use_file then
local fp = assert(io.open(outfile, "a"))
local str = string.format("[%-6s%s] %s: %s\n", nameupper, os.date(), lineinfo, msg)
fp:write(str)
fp:close()
end
-- Output to quickfix
if config.use_quickfix then
local formatted_msg = string.format("[%s] %s", nameupper, msg)
local qf_entry = {
-- remove the @ getinfo adds to the file path
filename = info.source:sub(2),
lnum = info.currentline,
col = 1,
text = formatted_msg,
}
vim.fn.setqflist({ qf_entry }, "a")
end
end
for i, x in ipairs(config.modes) do
-- log.info("these", "are", "separated")
obj[x.name] = function(...)
return log_at_level(i, x, make_string, ...)
end
-- log.fmt_info("These are %s strings", "formatted")
obj[("fmt_%s"):format(x.name)] = function(...)
return log_at_level(i, x, function(...)
local passed = { ... }
local fmt = table.remove(passed, 1)
local inspected = {}
for _, v in ipairs(passed) do
table.insert(inspected, vim.inspect(v))
end
return string.format(fmt, unpack(inspected))
end, ...)
end
-- log.lazy_info(expensive_to_calculate)
obj[("lazy_%s"):format(x.name)] = function()
return log_at_level(i, x, function(f)
return f()
end)
end
-- log.file_info("do not print")
obj[("file_%s"):format(x.name)] = function(vals, override)
local original_console = config.use_console
config.use_console = false
config.info_level = override.info_level
log_at_level(i, x, make_string, unpack(vals))
config.use_console = original_console
config.info_level = nil
end
end
return obj
end
log.new(default_config, true)
-- }}}
return log

View File

@ -0,0 +1,30 @@
local vim = vim
local M = {}
M._original_functions = {}
--- Override an lsp method default callback
--- @param method string
--- @param new_function function
function M.override(method, new_function)
if M._original_functions[method] == nil then
M._original_functions[method] = vim.lsp.callbacks[method]
end
vim.lsp.callbacks[method] = new_function
end
--- Get the original method callback
--- useful if you only want to override in some circumstances
---
--- @param method string
function M.get_original_function(method)
if M._original_functions[method] == nil then
M._original_functions[method] = vim.lsp.callbacks[method]
end
return M._original_functions[method]
end
return M

View File

@ -0,0 +1 @@
error "neorocks is no longer supported. Please use packer.nvim or other project for neorocks usage."

View File

@ -0,0 +1,18 @@
local get_lua_version = function()
if jit then
return {
lua = string.gsub(_VERSION, "Lua ", ""),
jit = not not string.find(jit.version, "LuaJIT"),
version = string.gsub(jit.version, "LuaJIT ", ""),
}
end
error("NEOROCKS: Unsupported Lua Versions", _VERSION)
end
return {
-- Is run in `--headless` mode.
is_headless = (#vim.api.nvim_list_uis() == 0),
lua_jit = get_lua_version(),
}

View File

@ -0,0 +1,100 @@
---@brief [[
---Operators that are functions.
---This is useful when you want to pass operators to higher order functions.
---Lua has no currying so we have to make a function for each operator.
---@brief ]]
return {
----------------------------------------------------------------------------
-- Comparison operators
----------------------------------------------------------------------------
lt = function(a, b)
return a < b
end,
le = function(a, b)
return a <= b
end,
eq = function(a, b)
return a == b
end,
ne = function(a, b)
return a ~= b
end,
ge = function(a, b)
return a >= b
end,
gt = function(a, b)
return a > b
end,
----------------------------------------------------------------------------
-- Arithmetic operators
----------------------------------------------------------------------------
add = function(a, b)
return a + b
end,
div = function(a, b)
return a / b
end,
floordiv = function(a, b)
return math.floor(a / b)
end,
intdiv = function(a, b)
local q = a / b
if a >= 0 then
return math.floor(q)
else
return math.ceil(q)
end
end,
mod = function(a, b)
return a % b
end,
mul = function(a, b)
return a * b
end,
neq = function(a)
return -a
end,
unm = function(a)
return -a
end, -- an alias
pow = function(a, b)
return a ^ b
end,
sub = function(a, b)
return a - b
end,
truediv = function(a, b)
return a / b
end,
----------------------------------------------------------------------------
-- String operators
----------------------------------------------------------------------------
concat = function(a, b)
return a .. b
end,
len = function(a)
return #a
end,
length = function(a)
return #a
end, -- an alias
----------------------------------------------------------------------------
-- Logical operators
----------------------------------------------------------------------------
land = function(a, b)
return a and b
end,
lor = function(a, b)
return a or b
end,
lnot = function(a)
return not a
end,
truth = function(a)
return not not a
end,
}

View File

@ -0,0 +1,899 @@
--- Path.lua
---
--- Goal: Create objects that are extremely similar to Python's `Path` Objects.
--- Reference: https://docs.python.org/3/library/pathlib.html
local bit = require "plenary.bit"
local uv = vim.loop
local F = require "plenary.functional"
local S_IF = {
-- S_IFDIR = 0o040000 # directory
DIR = 0x4000,
-- S_IFREG = 0o100000 # regular file
REG = 0x8000,
}
local path = {}
path.home = vim.loop.os_homedir()
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)()
path.root = (function()
if path.sep == "/" then
return function()
return "/"
end
else
return function(base)
base = base or vim.loop.cwd()
return base:sub(1, 1) .. ":\\"
end
end
end)()
path.S_IF = S_IF
local band = function(reg, value)
return bit.band(reg, value) == reg
end
local concat_paths = function(...)
return table.concat({ ... }, path.sep)
end
local function is_root(pathname)
if path.sep == "\\" then
return string.match(pathname, "^[A-Z]:\\?$")
end
return pathname == "/"
end
local _split_by_separator = (function()
local formatted = string.format("([^%s]+)", path.sep)
return function(filepath)
local t = {}
for str in string.gmatch(filepath, formatted) do
table.insert(t, str)
end
return t
end
end)()
local is_uri = function(filename)
return string.match(filename, "^%w+://") ~= nil
end
local is_absolute = function(filename, sep)
if sep == "\\" then
return string.match(filename, "^[A-Z]:\\.*$")
end
return string.sub(filename, 1, 1) == sep
end
local function _normalize_path(filename, cwd)
if is_uri(filename) then
return filename
end
-- handles redundant `./` in the middle
local redundant = path.sep .. "%." .. path.sep
if filename:match(redundant) then
filename = filename:gsub(redundant, path.sep)
end
local out_file = filename
local has = string.find(filename, path.sep .. "..", 1, true) or string.find(filename, ".." .. path.sep, 1, true)
if has then
local parts = _split_by_separator(filename)
local idx = 1
local initial_up_count = 0
repeat
if parts[idx] == ".." then
if idx == 1 then
initial_up_count = initial_up_count + 1
end
table.remove(parts, idx)
table.remove(parts, idx - 1)
if idx > 1 then
idx = idx - 2
else
idx = idx - 1
end
end
idx = idx + 1
until idx > #parts
local prefix = ""
if is_absolute(filename, path.sep) or #_split_by_separator(cwd) == initial_up_count then
prefix = path.root(filename)
end
out_file = prefix .. table.concat(parts, path.sep)
end
return out_file
end
local clean = function(pathname)
if is_uri(pathname) then
return pathname
end
-- Remove double path seps, it's annoying
pathname = pathname:gsub(path.sep .. path.sep, path.sep)
-- Remove trailing path sep if not root
if not is_root(pathname) and pathname:sub(-1) == path.sep then
return pathname:sub(1, -2)
end
return pathname
end
-- S_IFCHR = 0o020000 # character device
-- S_IFBLK = 0o060000 # block device
-- S_IFIFO = 0o010000 # fifo (named pipe)
-- S_IFLNK = 0o120000 # symbolic link
-- S_IFSOCK = 0o140000 # socket file
---@class Path
local Path = {
path = path,
}
local check_self = function(self)
if type(self) == "string" then
return Path:new(self)
end
return self
end
Path.__index = Path
-- TODO: Could use this to not have to call new... not sure
-- Path.__call = Path:new
Path.__div = function(self, other)
assert(Path.is_path(self))
assert(Path.is_path(other) or type(other) == "string")
return self:joinpath(other)
end
Path.__tostring = function(self)
return self.filename
end
-- TODO: See where we concat the table, and maybe we could make this work.
Path.__concat = function(self, other)
return self.filename .. other
end
Path.is_path = function(a)
return getmetatable(a) == Path
end
function Path:new(...)
local args = { ... }
if type(self) == "string" then
table.insert(args, 1, self)
self = Path
end
local path_input
if #args == 1 then
path_input = args[1]
else
path_input = args
end
-- If we already have a Path, it's fine.
-- Just return it
if Path.is_path(path_input) then
return path_input
end
-- TODO: Should probably remove and dumb stuff like double seps, periods in the middle, etc.
local sep = path.sep
if type(path_input) == "table" then
sep = path_input.sep or path.sep
path_input.sep = nil
end
local path_string
if type(path_input) == "table" then
-- TODO: It's possible this could be done more elegantly with __concat
-- But I'm unsure of what we'd do to make that happen
local path_objs = {}
for _, v in ipairs(path_input) do
if Path.is_path(v) then
table.insert(path_objs, v.filename)
else
assert(type(v) == "string")
table.insert(path_objs, v)
end
end
path_string = table.concat(path_objs, sep)
else
assert(type(path_input) == "string", vim.inspect(path_input))
path_string = path_input
end
local obj = {
filename = path_string,
_sep = sep,
-- Cached values
_absolute = uv.fs_realpath(path_string),
_cwd = uv.fs_realpath ".",
}
setmetatable(obj, Path)
return obj
end
function Path:_fs_filename()
return self:absolute() or self.filename
end
function Path:_stat()
return uv.fs_stat(self:_fs_filename()) or {}
-- local stat = uv.fs_stat(self:absolute())
-- if not self._absolute then return {} end
-- if not self._stat_result then
-- self._stat_result =
-- end
-- return self._stat_result
end
function Path:_st_mode()
return self:_stat().mode or 0
end
function Path:joinpath(...)
return Path:new(self.filename, ...)
end
function Path:absolute()
if self:is_absolute() then
return _normalize_path(self.filename, self._cwd)
else
return _normalize_path(self._absolute or table.concat({ self._cwd, self.filename }, self._sep), self._cwd)
end
end
function Path:exists()
return not vim.tbl_isempty(self:_stat())
end
function Path:expand()
if is_uri(self.filename) then
return self.filename
end
-- TODO support windows
local expanded
if string.find(self.filename, "~") then
expanded = string.gsub(self.filename, "^~", vim.loop.os_homedir())
elseif string.find(self.filename, "^%.") then
expanded = vim.loop.fs_realpath(self.filename)
if expanded == nil then
expanded = vim.fn.fnamemodify(self.filename, ":p")
end
elseif string.find(self.filename, "%$") then
local rep = string.match(self.filename, "([^%$][^/]*)")
local val = os.getenv(rep)
if val then
expanded = string.gsub(string.gsub(self.filename, rep, val), "%$", "")
else
expanded = nil
end
else
expanded = self.filename
end
return expanded and expanded or error "Path not valid"
end
function Path:make_relative(cwd)
if is_uri(self.filename) then
return self.filename
end
self.filename = clean(self.filename)
cwd = clean(F.if_nil(cwd, self._cwd, cwd))
if self.filename == cwd then
self.filename = "."
else
if cwd:sub(#cwd, #cwd) ~= path.sep then
cwd = cwd .. path.sep
end
if self.filename:sub(1, #cwd) == cwd then
self.filename = self.filename:sub(#cwd + 1, -1)
end
end
return self.filename
end
function Path:normalize(cwd)
if is_uri(self.filename) then
return self.filename
end
self:make_relative(cwd)
-- Substitute home directory w/ "~"
-- string.gsub is not useful here because usernames with dashes at the end
-- will be seen as a regexp pattern rather than a raw string
local home = path.home
if string.sub(path.home, -1) ~= path.sep then
home = home .. path.sep
end
local start, finish = string.find(self.filename, home, 1, true)
if start == 1 then
self.filename = "~" .. path.sep .. string.sub(self.filename, (finish + 1), -1)
end
return _normalize_path(clean(self.filename), self._cwd)
end
local function shorten_len(filename, len, exclude)
len = len or 1
exclude = exclude or { -1 }
local exc = {}
-- get parts in a table
local parts = {}
local empty_pos = {}
for m in (filename .. path.sep):gmatch("(.-)" .. path.sep) do
if m ~= "" then
parts[#parts + 1] = m
else
table.insert(empty_pos, #parts + 1)
end
end
for _, v in pairs(exclude) do
if v < 0 then
exc[v + #parts + 1] = true
else
exc[v] = true
end
end
local final_path_components = {}
local count = 1
for _, match in ipairs(parts) do
if not exc[count] and #match > len then
table.insert(final_path_components, string.sub(match, 1, len))
else
table.insert(final_path_components, match)
end
table.insert(final_path_components, path.sep)
count = count + 1
end
local l = #final_path_components -- so that we don't need to keep calculating length
table.remove(final_path_components, l) -- remove final slash
-- add back empty positions
for i = #empty_pos, 1, -1 do
table.insert(final_path_components, empty_pos[i], path.sep)
end
return table.concat(final_path_components)
end
local shorten = (function()
if jit and path.sep ~= "\\" then
local ffi = require "ffi"
ffi.cdef [[
typedef unsigned char char_u;
void shorten_dir(char_u *str);
]]
return function(filename)
if not filename or is_uri(filename) then
return filename
end
local c_str = ffi.new("char[?]", #filename + 1)
ffi.copy(c_str, filename)
ffi.C.shorten_dir(c_str)
return ffi.string(c_str)
end
end
return function(filename)
return shorten_len(filename, 1)
end
end)()
function Path:shorten(len, exclude)
assert(len ~= 0, "len must be at least 1")
if (len and len > 1) or exclude ~= nil then
return shorten_len(self.filename, len, exclude)
end
return shorten(self.filename)
end
function Path:mkdir(opts)
opts = opts or {}
local mode = opts.mode or 448 -- 0700 -> decimal
local parents = F.if_nil(opts.parents, false, opts.parents)
local exists_ok = F.if_nil(opts.exists_ok, true, opts.exists_ok)
local exists = self:exists()
if not exists_ok and exists then
error("FileExistsError:" .. self:absolute())
end
-- fs_mkdir returns nil if folder exists
if not uv.fs_mkdir(self:_fs_filename(), mode) and not exists then
if parents then
local dirs = self:_split()
local processed = ""
for _, dir in ipairs(dirs) do
if dir ~= "" then
local joined = concat_paths(processed, dir)
if processed == "" and self._sep == "\\" then
joined = dir
end
local stat = uv.fs_stat(joined) or {}
local file_mode = stat.mode or 0
if band(S_IF.REG, file_mode) then
error(string.format("%s is a regular file so we can't mkdir it", joined))
elseif band(S_IF.DIR, file_mode) then
processed = joined
else
if uv.fs_mkdir(joined, mode) then
processed = joined
else
error("We couldn't mkdir: " .. joined)
end
end
end
end
else
error "FileNotFoundError"
end
end
return true
end
function Path:rmdir()
if not self:exists() then
return
end
uv.fs_rmdir(self:absolute())
end
function Path:rename(opts)
opts = opts or {}
if not opts.new_name or opts.new_name == "" then
error "Please provide the new name!"
end
-- handles `.`, `..`, `./`, and `../`
if opts.new_name:match "^%.%.?/?\\?.+" then
opts.new_name = {
uv.fs_realpath(opts.new_name:sub(1, 3)),
opts.new_name:sub(4, #opts.new_name),
}
end
local new_path = Path:new(opts.new_name)
if new_path:exists() then
error "File or directory already exists!"
end
local status = uv.fs_rename(self:absolute(), new_path:absolute())
self.filename = new_path.filename
return status
end
--- Copy files or folders with defaults akin to GNU's `cp`.
---@param opts table: options to pass to toggling registered actions
---@field destination string|Path: target file path to copy to
---@field recursive bool: whether to copy folders recursively (default: false)
---@field override bool: whether to override files (default: true)
---@field interactive bool: confirm if copy would override; precedes `override` (default: false)
---@field respect_gitignore bool: skip folders ignored by all detected `gitignore`s (default: false)
---@field hidden bool: whether to add hidden files in recursively copying folders (default: true)
---@field parents bool: whether to create possibly non-existing parent dirs of `opts.destination` (default: false)
---@field exists_ok bool: whether ok if `opts.destination` exists, if so folders are merged (default: true)
---@return table {[Path of destination]: bool} indicating success of copy; nested tables constitute sub dirs
function Path:copy(opts)
opts = opts or {}
opts.recursive = F.if_nil(opts.recursive, false, opts.recursive)
opts.override = F.if_nil(opts.override, true, opts.override)
local dest = opts.destination
-- handles `.`, `..`, `./`, and `../`
if not Path.is_path(dest) then
if type(dest) == "string" and dest:match "^%.%.?/?\\?.+" then
dest = {
uv.fs_realpath(dest:sub(1, 3)),
dest:sub(4, #dest),
}
end
dest = Path:new(dest)
end
-- success is true in case file is copied, false otherwise
local success = {}
if not self:is_dir() then
if opts.interactive and dest:exists() then
vim.ui.select(
{ "Yes", "No" },
{ prompt = string.format("Overwrite existing %s?", dest:absolute()) },
function(_, idx)
success[dest] = uv.fs_copyfile(self:absolute(), dest:absolute(), { excl = not (idx == 1) }) or false
end
)
else
-- nil: not overriden if `override = false`
success[dest] = uv.fs_copyfile(self:absolute(), dest:absolute(), { excl = not opts.override }) or false
end
return success
end
-- dir
if opts.recursive then
dest:mkdir {
parents = F.if_nil(opts.parents, false, opts.parents),
exists_ok = F.if_nil(opts.exists_ok, true, opts.exists_ok),
}
local scan = require "plenary.scandir"
local data = scan.scan_dir(self.filename, {
respect_gitignore = F.if_nil(opts.respect_gitignore, false, opts.respect_gitignore),
hidden = F.if_nil(opts.hidden, true, opts.hidden),
depth = 1,
add_dirs = true,
})
for _, entry in ipairs(data) do
local entry_path = Path:new(entry)
local suffix = table.remove(entry_path:_split())
local new_dest = dest:joinpath(suffix)
-- clear destination as it might be Path table otherwise failing w/ extend
opts.destination = nil
local new_opts = vim.tbl_deep_extend("force", opts, { destination = new_dest })
-- nil: not overriden if `override = false`
success[new_dest] = entry_path:copy(new_opts) or false
end
return success
else
error(string.format("Warning: %s was not copied as `recursive=false`", self:absolute()))
end
end
function Path:touch(opts)
opts = opts or {}
local mode = opts.mode or 420
local parents = F.if_nil(opts.parents, false, opts.parents)
if self:exists() then
local new_time = os.time()
uv.fs_utime(self:_fs_filename(), new_time, new_time)
return
end
if parents then
Path:new(self:parent()):mkdir { parents = true }
end
local fd = uv.fs_open(self:_fs_filename(), "w", mode)
if not fd then
error("Could not create file: " .. self:_fs_filename())
end
uv.fs_close(fd)
return true
end
function Path:rm(opts)
opts = opts or {}
local recursive = F.if_nil(opts.recursive, false, opts.recursive)
if recursive then
local scan = require "plenary.scandir"
local abs = self:absolute()
-- first unlink all files
scan.scan_dir(abs, {
hidden = true,
on_insert = function(file)
uv.fs_unlink(file)
end,
})
local dirs = scan.scan_dir(abs, { add_dirs = true, hidden = true })
-- iterate backwards to clean up remaining dirs
for i = #dirs, 1, -1 do
uv.fs_rmdir(dirs[i])
end
-- now only abs is missing
uv.fs_rmdir(abs)
else
uv.fs_unlink(self:absolute())
end
end
-- Path:is_* {{{
function Path:is_dir()
-- TODO: I wonder when this would be better, if ever.
-- return self:_stat().type == 'directory'
return band(S_IF.DIR, self:_st_mode())
end
function Path:is_absolute()
return is_absolute(self.filename, self._sep)
end
-- }}}
function Path:_split()
return vim.split(self:absolute(), self._sep)
end
local _get_parent = (function()
local formatted = string.format("^(.+)%s[^%s]+", path.sep, path.sep)
return function(abs_path)
return abs_path:match(formatted)
end
end)()
function Path:parent()
return Path:new(_get_parent(self:absolute()) or path.root(self:absolute()))
end
function Path:parents()
local results = {}
local cur = self:absolute()
repeat
cur = _get_parent(cur)
table.insert(results, cur)
until not cur
table.insert(results, path.root(self:absolute()))
return results
end
function Path:is_file()
local stat = uv.fs_stat(self:expand())
if stat then
return stat.type == "file" and true or nil
end
end
-- TODO:
-- Maybe I can use libuv for this?
function Path:open() end
function Path:close() end
function Path:write(txt, flag, mode)
assert(flag, [[Path:write_text requires a flag! For example: 'w' or 'a']])
mode = mode or 438
local fd = assert(uv.fs_open(self:expand(), flag, mode))
assert(uv.fs_write(fd, txt, -1))
assert(uv.fs_close(fd))
end
-- TODO: Asyncify this and use vim.wait in the meantime.
-- This will allow other events to happen while we're waiting!
function Path:_read()
self = check_self(self)
local fd = assert(uv.fs_open(self:expand(), "r", 438)) -- for some reason test won't pass with absolute
local stat = assert(uv.fs_fstat(fd))
local data = assert(uv.fs_read(fd, stat.size, 0))
assert(uv.fs_close(fd))
return data
end
function Path:_read_async(callback)
vim.loop.fs_open(self.filename, "r", 438, function(err_open, fd)
if err_open then
print("We tried to open this file but couldn't. We failed with following error message: " .. err_open)
return
end
vim.loop.fs_fstat(fd, function(err_fstat, stat)
assert(not err_fstat, err_fstat)
if stat.type ~= "file" then
return callback ""
end
vim.loop.fs_read(fd, stat.size, 0, function(err_read, data)
assert(not err_read, err_read)
vim.loop.fs_close(fd, function(err_close)
assert(not err_close, err_close)
return callback(data)
end)
end)
end)
end)
end
function Path:read(callback)
if callback then
return self:_read_async(callback)
end
return self:_read()
end
function Path:head(lines)
lines = lines or 10
self = check_self(self)
local chunk_size = 256
local fd = uv.fs_open(self:expand(), "r", 438)
if not fd then
return
end
local stat = assert(uv.fs_fstat(fd))
if stat.type ~= "file" then
uv.fs_close(fd)
return nil
end
local data = ""
local index, count = 0, 0
while count < lines and index < stat.size do
local read_chunk = assert(uv.fs_read(fd, chunk_size, index))
local i = 0
for char in read_chunk:gmatch "." do
if char == "\n" then
count = count + 1
if count >= lines then
break
end
end
index = index + 1
i = i + 1
end
data = data .. read_chunk:sub(1, i)
end
assert(uv.fs_close(fd))
-- Remove potential newline at end of file
if data:sub(-1) == "\n" then
data = data:sub(1, -2)
end
return data
end
function Path:tail(lines)
lines = lines or 10
self = check_self(self)
local chunk_size = 256
local fd = uv.fs_open(self:expand(), "r", 438)
if not fd then
return
end
local stat = assert(uv.fs_fstat(fd))
if stat.type ~= "file" then
uv.fs_close(fd)
return nil
end
local data = ""
local index, count = stat.size - 1, 0
while count < lines and index > 0 do
local real_index = index - chunk_size
if real_index < 0 then
chunk_size = chunk_size + real_index
real_index = 0
end
local read_chunk = assert(uv.fs_read(fd, chunk_size, real_index))
local i = #read_chunk
while i > 0 do
local char = read_chunk:sub(i, i)
if char == "\n" then
count = count + 1
if count >= lines then
break
end
end
index = index - 1
i = i - 1
end
data = read_chunk:sub(i + 1, #read_chunk) .. data
end
assert(uv.fs_close(fd))
return data
end
function Path:readlines()
self = check_self(self)
local data = self:read()
data = data:gsub("\r", "")
return vim.split(data, "\n")
end
function Path:iter()
local data = self:readlines()
local i = 0
local n = #data
return function()
i = i + 1
if i <= n then
return data[i]
end
end
end
function Path:readbyterange(offset, length)
self = check_self(self)
local fd = uv.fs_open(self:expand(), "r", 438)
if not fd then
return
end
local stat = assert(uv.fs_fstat(fd))
if stat.type ~= "file" then
uv.fs_close(fd)
return nil
end
if offset < 0 then
offset = stat.size + offset
-- Windows fails if offset is < 0 even though offset is defined as signed
-- http://docs.libuv.org/en/v1.x/fs.html#c.uv_fs_read
if offset < 0 then
offset = 0
end
end
local data = ""
while #data < length do
local read_chunk = assert(uv.fs_read(fd, length - #data, offset))
if #read_chunk == 0 then
break
end
data = data .. read_chunk
offset = offset + #read_chunk
end
assert(uv.fs_close(fd))
return data
end
return Path

View File

@ -0,0 +1,484 @@
--- popup.lua
---
--- Wrapper to make the popup api from vim in neovim.
--- Hope to get this part merged in at some point in the future.
---
--- Please make sure to update "POPUP.md" with any changes and/or notes.
local Border = require "plenary.window.border"
local Window = require "plenary.window"
local utils = require "plenary.popup.utils"
local if_nil = vim.F.if_nil
local popup = {}
popup._pos_map = {
topleft = "NW",
topright = "NE",
botleft = "SW",
botright = "SE",
}
-- Keep track of hidden popups, so we can load them with popup.show()
popup._hidden = {}
-- Keep track of popup borders, so we don't have to pass them between functions
popup._borders = {}
local function dict_default(options, key, default)
if options[key] == nil then
return default[key]
else
return options[key]
end
end
-- Callbacks to be called later by popup.execute_callback
popup._callbacks = {}
-- Convert the positional {vim_options} to compatible neovim options and add them to {win_opts}
-- If an option is not given in {vim_options}, fall back to {default_opts}
local function add_position_config(win_opts, vim_options, default_opts)
default_opts = default_opts or {}
local cursor_relative_pos = function(pos_str, dim)
assert(string.find(pos_str, "^cursor"), "Invalid value for " .. dim)
win_opts.relative = "cursor"
local line = 0
if (pos_str):match "cursor%+(%d+)" then
line = line + tonumber((pos_str):match "cursor%+(%d+)")
elseif (pos_str):match "cursor%-(%d+)" then
line = line - tonumber((pos_str):match "cursor%-(%d+)")
end
return line
end
-- Feels like maxheight, minheight, maxwidth, minwidth will all be related
--
-- maxheight Maximum height of the contents, excluding border and padding.
-- minheight Minimum height of the contents, excluding border and padding.
-- maxwidth Maximum width of the contents, excluding border, padding and scrollbar.
-- minwidth Minimum width of the contents, excluding border, padding and scrollbar.
local width = if_nil(vim_options.width, default_opts.width)
local height = if_nil(vim_options.height, default_opts.height)
win_opts.width = utils.bounded(width, vim_options.minwidth, vim_options.maxwidth)
win_opts.height = utils.bounded(height, vim_options.minheight, vim_options.maxheight)
if vim_options.line and vim_options.line ~= 0 then
if type(vim_options.line) == "string" then
win_opts.row = cursor_relative_pos(vim_options.line, "row")
else
win_opts.row = vim_options.line - 1
end
else
win_opts.row = math.floor((vim.o.lines - win_opts.height) / 2)
end
if vim_options.col and vim_options.col ~= 0 then
if type(vim_options.col) == "string" then
win_opts.col = cursor_relative_pos(vim_options.col, "col")
else
win_opts.col = vim_options.col - 1
end
else
win_opts.col = math.floor((vim.o.columns - win_opts.width) / 2)
end
-- pos
--
-- Using "topleft", "topright", "botleft", "botright" defines what corner of the popup "line"
-- and "col" are used for. When not set "topleft" behaviour is used.
-- Alternatively "center" can be used to position the popup in the center of the Neovim window,
-- in which case "line" and "col" are ignored.
if vim_options.pos then
if vim_options.pos == "center" then
vim_options.line = 0
vim_options.col = 0
win_opts.anchor = "NW"
else
win_opts.anchor = popup._pos_map[vim_options.pos]
end
else
win_opts.anchor = "NW" -- This is the default, but makes `posinvert` easier to implement
end
-- , fixed When FALSE (the default), and:
-- , - "pos" is "botleft" or "topleft", and
-- , - "wrap" is off, and
-- , - the popup would be truncated at the right edge of
-- , the screen, then
-- , the popup is moved to the left so as to fit the
-- , contents on the screen. Set to TRUE to disable this.
end
function popup.create(what, vim_options)
vim_options = vim.deepcopy(vim_options)
local bufnr
if type(what) == "number" then
bufnr = what
else
bufnr = vim.api.nvim_create_buf(false, true)
assert(bufnr, "Failed to create buffer")
vim.api.nvim_buf_set_option(bufnr, "bufhidden", "wipe")
-- TODO: Handle list of lines
if type(what) == "string" then
what = { what }
else
assert(type(what) == "table", '"what" must be a table')
end
-- padding List with numbers, defining the padding
-- above/right/below/left of the popup (similar to CSS).
-- An empty list uses a padding of 1 all around. The
-- padding goes around the text, inside any border.
-- Padding uses the 'wincolor' highlight.
-- Example: [1, 2, 1, 3] has 1 line of padding above, 2
-- columns on the right, 1 line below and 3 columns on
-- the left.
if vim_options.padding then
local pad_top, pad_right, pad_below, pad_left
if vim.tbl_isempty(vim_options.padding) then
pad_top = 1
pad_right = 1
pad_below = 1
pad_left = 1
else
local padding = vim_options.padding
pad_top = padding[1] or 0
pad_right = padding[2] or 0
pad_below = padding[3] or 0
pad_left = padding[4] or 0
end
local left_padding = string.rep(" ", pad_left)
local right_padding = string.rep(" ", pad_right)
for index = 1, #what do
what[index] = string.format("%s%s%s", left_padding, what[index], right_padding)
end
for _ = 1, pad_top do
table.insert(what, 1, "")
end
for _ = 1, pad_below do
table.insert(what, "")
end
end
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, what)
end
local option_defaults = {
posinvert = true,
zindex = 50,
}
vim_options.width = if_nil(vim_options.width, 1)
if type(what) == "number" then
vim_options.height = vim.api.nvim_buf_line_count(what)
else
for _, v in ipairs(what) do
vim_options.width = math.max(vim_options.width, #v)
end
vim_options.height = #what
end
local win_opts = {}
win_opts.relative = "editor"
win_opts.style = "minimal"
-- Add positional and sizing config to win_opts
add_position_config(win_opts, vim_options, { width = 1, height = 1 })
-- posinvert, When FALSE the value of "pos" is always used. When
-- , TRUE (the default) and the popup does not fit
-- , vertically and there is more space on the other side
-- , then the popup is placed on the other side of the
-- , position indicated by "line".
if dict_default(vim_options, "posinvert", option_defaults) then
if win_opts.anchor == "NW" or win_opts.anchor == "NE" then
if win_opts.row + win_opts.height > vim.o.lines and win_opts.row * 2 > vim.o.lines then
-- Don't know why, but this is how vim adjusts it
win_opts.row = win_opts.row - win_opts.height - 2
end
elseif win_opts.anchor == "SW" or win_opts.anchor == "SE" then
if win_opts.row - win_opts.height < 0 and win_opts.row * 2 < vim.o.lines then
-- Don't know why, but this is how vim adjusts it
win_opts.row = win_opts.row + win_opts.height + 2
end
end
end
-- textprop, When present the popup is positioned next to a text
-- , property with this name and will move when the text
-- , property moves. Use an empty string to remove. See
-- , |popup-textprop-pos|.
-- related:
-- textpropwin
-- textpropid
-- zindex, Priority for the popup, default 50. Minimum value is
-- , 1, maximum value is 32000.
local zindex = dict_default(vim_options, "zindex", option_defaults)
win_opts.zindex = utils.bounded(zindex, 1, 32000)
-- noautocmd, undocumented vim default per https://github.com/vim/vim/issues/5737
win_opts.noautocmd = if_nil(vim_options.noautocmd, true)
-- focusable,
-- vim popups are not focusable windows
win_opts.focusable = if_nil(vim_options.focusable, false)
local win_id
if vim_options.hidden then
assert(false, "I have not implemented this yet and don't know how")
else
win_id = vim.api.nvim_open_win(bufnr, false, win_opts)
end
-- Moved, handled after since we need the window ID
if vim_options.moved then
if vim_options.moved == "any" then
vim.lsp.util.close_preview_autocmd({ "CursorMoved", "CursorMovedI" }, win_id)
elseif vim_options.moved == "word" then
-- TODO: Handle word, WORD, expr, and the range functions... which seem hard?
end
else
local silent = false
vim.cmd(
string.format(
"autocmd BufDelete %s <buffer=%s> ++once ++nested :lua require('plenary.window').try_close(%s, true)",
(silent and "<silent>") or "",
bufnr,
win_id
)
)
end
if vim_options.time then
local timer = vim.loop.new_timer()
timer:start(
vim_options.time,
0,
vim.schedule_wrap(function()
Window.try_close(win_id, false)
end)
)
end
-- Buffer Options
if vim_options.cursorline then
vim.api.nvim_win_set_option(win_id, "cursorline", true)
end
if vim_options.wrap ~= nil then
-- set_option wrap should/will trigger autocmd, see https://github.com/neovim/neovim/pull/13247
if vim_options.noautocmd then
vim.cmd(string.format("noautocmd lua vim.api.nvim_set_option(%s, wrap, %s)", win_id, vim_options.wrap))
else
vim.api.nvim_win_set_option(win_id, "wrap", vim_options.wrap)
end
end
-- ===== Not Implemented Options =====
-- flip: not implemented at the time of writing
-- Mouse:
-- mousemoved: no idea how to do the things with the mouse, so it's an exercise for the reader.
-- drag: mouses are hard
-- resize: mouses are hard
-- close: mouses are hard
--
-- scrollbar
-- scrollbarhighlight
-- thumbhighlight
-- tabpage: seems useless
-- Create border
local should_show_border = nil
local border_options = {}
-- border List with numbers, defining the border thickness
-- above/right/below/left of the popup (similar to CSS).
-- Only values of zero and non-zero are recognized.
-- An empty list uses a border all around.
if vim_options.border then
should_show_border = true
if type(vim_options.border) == "boolean" or vim.tbl_isempty(vim_options.border) then
border_options.border_thickness = Border._default_thickness
elseif #vim_options.border == 4 then
border_options.border_thickness = {
top = utils.bounded(vim_options.border[1], 0, 1),
right = utils.bounded(vim_options.border[2], 0, 1),
bot = utils.bounded(vim_options.border[3], 0, 1),
left = utils.bounded(vim_options.border[4], 0, 1),
}
else
error(string.format("Invalid configuration for border: %s", vim.inspect(vim_options.border)))
end
elseif vim_options.border == false then
should_show_border = false
end
if (should_show_border == nil or should_show_border) and vim_options.borderchars then
should_show_border = true
-- borderchars List with characters, defining the character to use
-- for the top/right/bottom/left border. Optionally
-- followed by the character to use for the
-- topleft/topright/botright/botleft corner.
-- Example: ['-', '|', '-', '|', '┌', '┐', '┘', '└']
-- When the list has one character it is used for all.
-- When the list has two characters the first is used for
-- the border lines, the second for the corners.
-- By default a double line is used all around when
-- 'encoding' is "utf-8" and 'ambiwidth' is "single",
-- otherwise ASCII characters are used.
local b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft
if vim_options.borderchars == nil then
b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft =
"", "", "", "", "", "", "", ""
elseif #vim_options.borderchars == 1 then
local b_char = vim_options.borderchars[1]
b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft =
b_char, b_char, b_char, b_char, b_char, b_char, b_char, b_char
elseif #vim_options.borderchars == 2 then
local b_char = vim_options.borderchars[1]
local c_char = vim_options.borderchars[2]
b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft =
b_char, b_char, b_char, b_char, c_char, c_char, c_char, c_char
elseif #vim_options.borderchars == 8 then
b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft = unpack(vim_options.borderchars)
else
error(string.format 'Not enough arguments for "borderchars"')
end
border_options.top = b_top
border_options.bot = b_bot
border_options.right = b_right
border_options.left = b_left
border_options.topleft = b_topleft
border_options.topright = b_topright
border_options.botright = b_botright
border_options.botleft = b_botleft
end
-- title
if vim_options.title then
-- TODO: Check out how title works with weird border combos.
border_options.title = vim_options.title
end
local border = nil
if should_show_border then
border_options.focusable = vim_options.border_focusable
border_options.highlight = vim_options.borderhighlight and string.format("Normal:%s", vim_options.borderhighlight)
border_options.titlehighlight = vim_options.titlehighlight
border = Border:new(bufnr, win_id, win_opts, border_options)
popup._borders[win_id] = border
end
if vim_options.highlight then
vim.api.nvim_win_set_option(
win_id,
"winhl",
string.format("Normal:%s,EndOfBuffer:%s", vim_options.highlight, vim_options.highlight)
)
end
-- enter
local should_enter = vim_options.enter
if should_enter == nil then
should_enter = true
end
if should_enter then
-- set focus after border creation so that it's properly placed (especially
-- in relative cursor layout)
if vim_options.noautocmd then
vim.cmd("noautocmd lua vim.api.nvim_set_current_win(" .. win_id .. ")")
else
vim.api.nvim_set_current_win(win_id)
end
end
-- callback
if vim_options.callback then
popup._callbacks[bufnr] = function()
-- (jbyuki): Giving win_id is pointless here because it's closed right afterwards
-- but it might make more sense once hidden is implemented
local row, _ = unpack(vim.api.nvim_win_get_cursor(win_id))
vim_options.callback(win_id, what[row])
vim.api.nvim_win_close(win_id, true)
end
vim.api.nvim_buf_set_keymap(
bufnr,
"n",
"<CR>",
'<cmd>lua require"popup".execute_callback(' .. bufnr .. ")<CR>",
{ noremap = true }
)
end
-- TODO: Perhaps there's a way to return an object that looks like a window id,
-- but actually has some extra metadata about it.
--
-- This would make `hidden` a lot easier to manage
return win_id, {
win_id = win_id,
border = border,
}
end
-- Move popup with window id {win_id} to the position specified with {vim_options}.
-- {vim_options} may contain the following items that determine the popup position/size:
-- - line
-- - col
-- - height
-- - width
-- - maxheight/minheight
-- - maxwidth/minwidth
-- - pos
-- Unimplemented vim options here include: fixed
function popup.move(win_id, vim_options)
-- Create win_options
local win_opts = {}
win_opts.relative = "editor"
local current_pos = vim.api.nvim_win_get_position(win_id)
local default_opts = {
width = vim.api.nvim_win_get_width(win_id),
height = vim.api.nvim_win_get_height(win_id),
row = current_pos[1],
col = current_pos[2],
}
-- Add positional and sizing config to win_opts
add_position_config(win_opts, vim_options, default_opts)
-- Update content window
vim.api.nvim_win_set_config(win_id, win_opts)
-- Update border window (if present)
local border = popup._borders[win_id]
if border ~= nil then
border:move(win_opts, border._border_win_options)
end
end
function popup.execute_callback(bufnr)
if popup._callbacks[bufnr] then
local wrapper = popup._callbacks[bufnr]
wrapper()
popup._callbacks[bufnr] = nil
end
end
return popup

View File

@ -0,0 +1,33 @@
local utils = {}
utils.bounded = function(value, min, max)
min = min or 0
max = max or math.huge
if min then
value = math.max(value, min)
end
if max then
value = math.min(value, max)
end
return value
end
utils.apply_defaults = function(original, defaults)
if original == nil then
original = {}
end
original = vim.deepcopy(original)
for k, v in pairs(defaults) do
if original[k] == nil then
original[k] = v
end
end
return original
end
return utils

View File

@ -0,0 +1,31 @@
local profile = {}
-- bundled version of upstream jit.p until LuaJIT is updated to include
-- https://github.com/LuaJIT/LuaJIT/commit/95140c50010c0557af66dac944403a1a65dd312c
local p = require'plenary.profile.p'
---start profiling using LuaJIT profiler
---@param out name and path of log file
---@param opts table of options
--- flame (bool, default false) write log in flamegraph format
-- (see https://github.com/jonhoo/inferno)
function profile.start(out, opts)
out = out or "profile.log"
opts = opts or {}
popts = "10,i1,s,m0"
if opts.flame then popts = popts .. ",G" end
p.start(popts, out)
end
---stop profiling
profile.stop = p.stop
function profile.benchmark(iterations, f, ...)
local start_time = vim.loop.hrtime()
for _ = 1, iterations do
f(...)
end
return (vim.loop.hrtime() - start_time) / 1E9
end
return profile

View File

@ -0,0 +1,252 @@
--[[ Copyright (c) 2018-2020, Charles Mallah ]]
-- Released with MIT License
--
-- Originally link:
-- https://github.com/charlesmallah/lua-profiler
--
-- Hopefully will add some better neovim stuff in the future.
-- Shoutout to @clason for finding this.
---------------------------------------|
--- Configuration
--
---------------------------------------|
local PROFILER_FILENAME = "lua/telescope/profile/lua_profiler.lua" -- Location and name of profiler (to remove itself from reports);
-- e.g. if this is in a 'tool' folder, rename this as: "tool/profiler.lua"
local EMPTY_TIME = "0.0000" -- Detect empty time, replace with tag below
local emptyToThis = "~"
local fileWidth = 75
local funcWidth = 22
local lineWidth = 6
local timeWidth = 7
local relaWidth = 6
local callWidth = 4
local reportSaved = " > Report saved to"
local formatOutputHeader = "| %-"..fileWidth.."s: %-"..funcWidth.."s: %-"..lineWidth.."s: %-"..timeWidth.."s: %-"..relaWidth.."s: %-"..callWidth.."s|\n"
local formatOutputTitle = "%-"..fileWidth.."."..fileWidth.."s: %-"..funcWidth.."."..funcWidth.."s: %-"..lineWidth.."s" -- File / Function / Line count
local formatOutput = "| %s: %-"..timeWidth.."s: %-"..relaWidth.."s: %-"..callWidth.."s|\n" -- Time / Relative / Called
local formatTotalTime = "TOTAL TIME = %f s\n"
local formatFunLine = "%"..(lineWidth - 2).."i"
local formatFunTime = "%04.4f"
local formatFunRelative = "%03.1f"
local formatFunCount = "%"..(callWidth - 1).."i"
local formatHeader = string.format(formatOutputHeader, "FILE", "FUNCTION", "LINE", "TIME", "%", "#")
---------------------------------------|
--- Locals
--
---------------------------------------|
local module = {}
local getTime = os.clock
local string = string
local debug = debug
local table = table
local TABL_REPORT_CACHE = {}
local TABL_REPORTS = {}
local reportCount = 0
local startTime = 0
local stopTime = 0
local printFun = nil
local verbosePrint = false
local function functionReport(information)
local src = information.short_src
if src == nil then
src = "<C>"
elseif string.sub(src, #src - 3, #src) == ".lua" then
src = string.sub(src, 1, #src - 4)
end
local name = information.name
if name == nil then
name = "Anon"
elseif string.sub(name, #name - 1, #name) == "_l" then
name = string.sub(name, 1, #name - 2)
end
local title = string.format(formatOutputTitle,
src, name,
string.format(formatFunLine, information.linedefined or 0))
local funcReport = TABL_REPORT_CACHE[title]
if not funcReport then
funcReport = {
title = string.format(formatOutputTitle,
src, name,
string.format(formatFunLine, information.linedefined or 0)),
count = 0,
timer = 0,
}
TABL_REPORT_CACHE[title] = funcReport
reportCount = reportCount + 1
TABL_REPORTS[reportCount] = funcReport
end
return funcReport
end
local onDebugHook = function(hookType)
local information = debug.getinfo(2, "nS")
if hookType == "call" then
local funcReport = functionReport(information)
funcReport.callTime = getTime()
funcReport.count = funcReport.count + 1
elseif hookType == "return" then
local funcReport = functionReport(information)
if funcReport.callTime and funcReport.count > 0 then
funcReport.timer = funcReport.timer + (getTime() - funcReport.callTime)
end
end
end
local function charRepetition(n, character)
local s = ""
character = character or " "
for _ = 1, n do
s = s..character
end
return s
end
local function singleSearchReturn(str, search)
for _ in string.gmatch(str, search) do
do return true end
end
return false
end
local divider = charRepetition(#formatHeader - 1, "-").."\n"
---------------------------------------|
--- Functions
--
---------------------------------------|
--- Attach a print function to the profiler, to receive a single string parameter
--
function module.attachPrintFunction(fn, verbose)
printFun = fn
if verbose ~= nil then
verbosePrint = verbose
end
end
---
--
function module.start()
TABL_REPORT_CACHE = {}
TABL_REPORTS = {}
reportCount = 0
startTime = getTime()
stopTime = nil
debug.sethook(onDebugHook, "cr", 0)
end
---
--
function module.stop()
stopTime = getTime()
debug.sethook()
end
--- Writes the profile report to file
--
function module.report(filename)
if stopTime == nil then
module.stop()
end
if reportCount > 0 then
filename = filename or "profiler.log"
table.sort(TABL_REPORTS, function(a, b) return a.timer > b.timer end)
local file = io.open(filename, "w+")
if reportCount > 0 then
local divide = false
local totalTime = stopTime - startTime
local totalTimeOutput = " > "..string.format(formatTotalTime, totalTime)
file:write(totalTimeOutput)
if printFun ~= nil then
printFun(totalTimeOutput)
end
file:write("\n"..divider)
file:write(formatHeader)
file:write(divider)
for i = 1, reportCount do
local funcReport = TABL_REPORTS[i]
if funcReport.count > 0 and funcReport.timer <= totalTime then
local printThis = true
if PROFILER_FILENAME ~= "" then
if singleSearchReturn(funcReport.title, PROFILER_FILENAME) then
printThis = false
end
end
-- Remove line if not needed
if printThis == true then
if singleSearchReturn(funcReport.title, "[[C]]") then
printThis = false
end
end
if printThis == true then
local count = string.format(formatFunCount, funcReport.count)
local timer = string.format(formatFunTime, funcReport.timer)
local relTime = string.format(formatFunRelative, (funcReport.timer / totalTime) * 100)
if divide == false and timer == EMPTY_TIME then
file:write(divider)
divide = true
end
-- Replace
if timer == EMPTY_TIME then
timer = emptyToThis
relTime = emptyToThis
end
-- Build final line
local outputLine = string.format(formatOutput, funcReport.title, timer, relTime, count)
file:write(outputLine)
-- This is a verbose print to the printFun, however maybe make this smaller for on screen debug?
if printFun ~= nil and verbosePrint == true then
printFun(outputLine)
end
end
end
end
file:write(divider)
end
file:close()
if printFun ~= nil then
printFun(reportSaved.."'"..filename.."'")
end
end
end
--- End
--
return module

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,312 @@
----------------------------------------------------------------------------
-- LuaJIT profiler.
--
-- Copyright (C) 2005-2021 Mike Pall. All rights reserved.
-- Released under the MIT license. See Copyright Notice in luajit.h
----------------------------------------------------------------------------
--
-- This module is a simple command line interface to the built-in
-- low-overhead profiler of LuaJIT.
--
-- The lower-level API of the profiler is accessible via the "jit.profile"
-- module or the luaJIT_profile_* C API.
--
-- Example usage:
--
-- luajit -jp myapp.lua
-- luajit -jp=s myapp.lua
-- luajit -jp=-s myapp.lua
-- luajit -jp=vl myapp.lua
-- luajit -jp=G,profile.txt myapp.lua
--
-- The following dump features are available:
--
-- f Stack dump: function name, otherwise module:line. Default mode.
-- F Stack dump: ditto, but always prepend module.
-- l Stack dump: module:line.
-- <number> stack dump depth (callee < caller). Default: 1.
-- -<number> Inverse stack dump depth (caller > callee).
-- s Split stack dump after first stack level. Implies abs(depth) >= 2.
-- p Show full path for module names.
-- v Show VM states. Can be combined with stack dumps, e.g. vf or fv.
-- z Show zones. Can be combined with stack dumps, e.g. zf or fz.
-- r Show raw sample counts. Default: show percentages.
-- a Annotate excerpts from source code files.
-- A Annotate complete source code files.
-- G Produce raw output suitable for graphical tools (e.g. flame graphs).
-- m<number> Minimum sample percentage to be shown. Default: 3.
-- i<number> Sampling interval in milliseconds. Default: 10.
--
----------------------------------------------------------------------------
-- Cache some library functions and objects.
local jit = require("jit")
assert(jit.version_num == 20100, "LuaJIT core/library version mismatch")
local profile = require("jit.profile")
local vmdef = require("jit.vmdef")
local math = math
local pairs, ipairs, tonumber, floor = pairs, ipairs, tonumber, math.floor
local sort, format = table.sort, string.format
local stdout = io.stdout
local zone -- Load jit.zone module on demand.
-- Output file handle.
local out
------------------------------------------------------------------------------
local prof_ud
local prof_states, prof_split, prof_min, prof_raw, prof_fmt, prof_depth
local prof_ann, prof_count1, prof_count2, prof_samples
local map_vmmode = {
N = "Compiled",
I = "Interpreted",
C = "C code",
G = "Garbage Collector",
J = "JIT Compiler",
}
-- Profiler callback.
local function prof_cb(th, samples, vmmode)
prof_samples = prof_samples + samples
local key_stack, key_stack2, key_state
-- Collect keys for sample.
if prof_states then
if prof_states == "v" then
key_state = map_vmmode[vmmode] or vmmode
else
key_state = zone:get() or "(none)"
end
end
if prof_fmt then
key_stack = profile.dumpstack(th, prof_fmt, prof_depth)
key_stack = key_stack:gsub("%[builtin#(%d+)%]", function(x)
return vmdef.ffnames[tonumber(x)]
end)
if prof_split == 2 then
local k1, k2 = key_stack:match("(.-) [<>] (.*)")
if k2 then key_stack, key_stack2 = k1, k2 end
elseif prof_split == 3 then
key_stack2 = profile.dumpstack(th, "l", 1)
end
end
-- Order keys.
local k1, k2
if prof_split == 1 then
if key_state then
k1 = key_state
if key_stack then k2 = key_stack end
end
elseif key_stack then
k1 = key_stack
if key_stack2 then k2 = key_stack2 elseif key_state then k2 = key_state end
end
-- Coalesce samples in one or two levels.
if k1 then
local t1 = prof_count1
t1[k1] = (t1[k1] or 0) + samples
if k2 then
local t2 = prof_count2
local t3 = t2[k1]
if not t3 then t3 = {}; t2[k1] = t3 end
t3[k2] = (t3[k2] or 0) + samples
end
end
end
------------------------------------------------------------------------------
-- Show top N list.
local function prof_top(count1, count2, samples, indent)
local t, n = {}, 0
for k in pairs(count1) do
n = n + 1
t[n] = k
end
sort(t, function(a, b) return count1[a] > count1[b] end)
for i=1,n do
local k = t[i]
local v = count1[k]
local pct = floor(v*100/samples + 0.5)
if pct < prof_min then break end
if not prof_raw then
out:write(format("%s%2d%% %s\n", indent, pct, k))
elseif prof_raw == "r" then
out:write(format("%s%5d %s\n", indent, v, k))
else
out:write(format("%s %d\n", k, v))
end
if count2 then
local r = count2[k]
if r then
prof_top(r, nil, v, (prof_split == 3 or prof_split == 1) and " -- " or
(prof_depth < 0 and " -> " or " <- "))
end
end
end
end
-- Annotate source code
local function prof_annotate(count1, samples)
local files = {}
local ms = 0
for k, v in pairs(count1) do
local pct = floor(v*100/samples + 0.5)
ms = math.max(ms, v)
if pct >= prof_min then
local file, line = k:match("^(.*):(%d+)$")
if not file then file = k; line = 0 end
local fl = files[file]
if not fl then fl = {}; files[file] = fl; files[#files+1] = file end
line = tonumber(line)
fl[line] = prof_raw and v or pct
end
end
sort(files)
local fmtv, fmtn = " %3d%% | %s\n", " | %s\n"
if prof_raw then
local n = math.max(5, math.ceil(math.log10(ms)))
fmtv = "%"..n.."d | %s\n"
fmtn = (" "):rep(n).." | %s\n"
end
local ann = prof_ann
for _, file in ipairs(files) do
local f0 = file:byte()
if f0 == 40 or f0 == 91 then
out:write(format("\n====== %s ======\n[Cannot annotate non-file]\n", file))
break
end
local fp, err = io.open(file)
if not fp then
out:write(format("====== ERROR: %s: %s\n", file, err))
break
end
out:write(format("\n====== %s ======\n", file))
local fl = files[file]
local n, show = 1, false
if ann ~= 0 then
for i=1,ann do
if fl[i] then show = true; out:write("@@ 1 @@\n"); break end
end
end
for line in fp:lines() do
if line:byte() == 27 then
out:write("[Cannot annotate bytecode file]\n")
break
end
local v = fl[n]
if ann ~= 0 then
local v2 = fl[n+ann]
if show then
if v2 then show = n+ann elseif v then show = n
elseif show+ann < n then show = false end
elseif v2 then
show = n+ann
out:write(format("@@ %d @@\n", n))
end
if not show then goto next end
end
if v then
out:write(format(fmtv, v, line))
else
out:write(format(fmtn, line))
end
::next::
n = n + 1
end
fp:close()
end
end
------------------------------------------------------------------------------
-- Finish profiling and dump result.
local function prof_finish()
if prof_ud then
profile.stop()
local samples = prof_samples
if samples == 0 then
if prof_raw ~= true then out:write("[No samples collected]\n") end
return
end
if prof_ann then
prof_annotate(prof_count1, samples)
else
prof_top(prof_count1, prof_count2, samples, "")
end
prof_count1 = nil
prof_count2 = nil
prof_ud = nil
if out ~= stdout then out:close() end
end
end
-- Start profiling.
local function prof_start(mode)
local interval = ""
mode = mode:gsub("i%d*", function(s) interval = s; return "" end)
prof_min = 3
mode = mode:gsub("m(%d+)", function(s) prof_min = tonumber(s); return "" end)
prof_depth = 1
mode = mode:gsub("%-?%d+", function(s) prof_depth = tonumber(s); return "" end)
local m = {}
for c in mode:gmatch(".") do m[c] = c end
prof_states = m.z or m.v
if prof_states == "z" then zone = require("jit.zone") end
local scope = m.l or m.f or m.F or (prof_states and "" or "f")
local flags = (m.p or "")
prof_raw = m.r
if m.s then
prof_split = 2
if prof_depth == -1 or m["-"] then prof_depth = -2
elseif prof_depth == 1 then prof_depth = 2 end
elseif mode:find("[fF].*l") then
scope = "l"
prof_split = 3
else
prof_split = (scope == "" or mode:find("[zv].*[lfF]")) and 1 or 0
end
prof_ann = m.A and 0 or (m.a and 3)
if prof_ann then
scope = "l"
prof_fmt = "pl"
prof_split = 0
prof_depth = 1
elseif m.G and scope ~= "" then
prof_fmt = flags..scope.."Z;"
prof_depth = -100
prof_raw = true
prof_min = 0
elseif scope == "" then
prof_fmt = false
else
local sc = prof_split == 3 and m.f or m.F or scope
prof_fmt = flags..sc..(prof_depth >= 0 and "Z < " or "Z > ")
end
prof_count1 = {}
prof_count2 = {}
prof_samples = 0
profile.start(scope:lower()..interval, prof_cb)
prof_ud = newproxy(true)
getmetatable(prof_ud).__gc = prof_finish
end
------------------------------------------------------------------------------
local function start(mode, outfile)
if not outfile then outfile = os.getenv("LUAJIT_PROFILEFILE") end
if outfile then
out = outfile == "-" and stdout or assert(io.open(outfile, "w"))
else
out = stdout
end
prof_start(mode or "f")
end
-- Public module functions.
return {
start = start, -- For -j command line option.
stop = prof_finish
}

View File

@ -0,0 +1,36 @@
local reload = {}
reload.reload_module = function(module_name, starts_with_only)
-- Default to starts with only
if starts_with_only == nil then
starts_with_only = true
end
-- TODO: Might need to handle cpath / compiled lua packages? Not sure.
local matcher
if not starts_with_only then
matcher = function(pack)
return string.find(pack, module_name, 1, true)
end
else
local module_name_pattern = vim.pesc(module_name)
matcher = function(pack)
return string.find(pack, "^" .. module_name_pattern)
end
end
-- Handle impatient.nvim automatically.
local luacache = (_G.__luacache or {}).cache
for pack, _ in pairs(package.loaded) do
if matcher(pack) then
package.loaded[pack] = nil
if luacache then
luacache[pack] = nil
end
end
end
end
return reload

View File

@ -0,0 +1,28 @@
local floatwin = require "plenary.window.float"
local run = {}
run.with_displayed_output = function(title_text, cmd, opts)
local views = floatwin.centered_with_top_win(title_text)
local job_id = vim.fn.termopen(cmd)
local count = 0
while not vim.wait(1000, function()
return vim.fn.jobwait({ job_id }, 0)[1] == -1
end) do
vim.cmd [[normal! G]]
count = count + 1
if count == 10 then
break
end
end
vim.fn.win_gotoid(views.win_id)
vim.cmd [[startinsert]]
return views.bufnr, views.win_id
end
return run

View File

@ -0,0 +1,602 @@
local Path = require "plenary.path"
local os_sep = Path.path.sep
local F = require "plenary.functional"
local uv = vim.loop
local m = {}
local make_gitignore = function(basepath)
local patterns = {}
local valid = false
for _, v in ipairs(basepath) do
local p = Path:new(v .. os_sep .. ".gitignore")
if p:exists() then
valid = true
patterns[v] = { ignored = {}, negated = {} }
for l in p:iter() do
local prefix = l:sub(1, 1)
local negated = prefix == "!"
if negated then
l = l:sub(2)
prefix = l:sub(1, 1)
end
if prefix == "/" then
l = v .. l
end
if not (prefix == "" or prefix == "#") then
local el = vim.trim(l)
el = el:gsub("%-", "%%-")
el = el:gsub("%.", "%%.")
el = el:gsub("/%*%*/", "/%%w+/")
el = el:gsub("%*%*", "")
el = el:gsub("%*", "%%w+")
el = el:gsub("%?", "%%w")
if el ~= "" then
table.insert(negated and patterns[v].negated or patterns[v].ignored, el)
end
end
end
end
end
if not valid then
return nil
end
return function(bp, entry)
for _, v in ipairs(bp) do
if entry:find(v, 1, true) then
local negated = false
for _, w in ipairs(patterns[v].ignored) do
if not negated and entry:match(w) then
for _, inverse in ipairs(patterns[v].negated) do
if not negated and entry:match(inverse) then
negated = true
end
end
if not negated then
return false
end
end
end
end
end
return true
end
end
-- exposed for testing
m.__make_gitignore = make_gitignore
local handle_depth = function(base_paths, entry, depth)
for _, v in ipairs(base_paths) do
if entry:find(v, 1, true) then
local cut = entry:sub(#v + 1, -1)
cut = cut:sub(1, 1) == os_sep and cut:sub(2, -1) or cut
local _, count = cut:gsub(os_sep, "")
if depth <= (count + 1) then
return nil
end
end
end
return entry
end
local gen_search_pat = function(pattern)
if type(pattern) == "string" then
return function(entry)
return entry:match(pattern)
end
elseif type(pattern) == "table" then
return function(entry)
for _, v in ipairs(pattern) do
if entry:match(v) then
return true
end
end
return false
end
elseif type(pattern) == "function" then
return pattern
end
end
local process_item = function(opts, name, typ, current_dir, next_dir, bp, data, giti, msp)
if opts.hidden or name:sub(1, 1) ~= "." then
if typ == "directory" then
local entry = current_dir .. os_sep .. name
if opts.depth then
table.insert(next_dir, handle_depth(bp, entry, opts.depth))
else
table.insert(next_dir, entry)
end
if opts.add_dirs or opts.only_dirs then
if not giti or giti(bp, entry .. "/") then
if not msp or msp(entry) then
table.insert(data, entry)
if opts.on_insert then
opts.on_insert(entry, typ)
end
end
end
end
elseif not opts.only_dirs then
local entry = current_dir .. os_sep .. name
if not giti or giti(bp, entry) then
if not msp or msp(entry) then
table.insert(data, entry)
if opts.on_insert then
opts.on_insert(entry, typ)
end
end
end
end
end
end
--- m.scan_dir
-- Search directory recursive and syncronous
-- @param path: string or table
-- string has to be a valid path
-- table has to be a array of valid paths
-- @param opts: table to change behavior
-- opts.hidden (bool): if true hidden files will be added
-- opts.add_dirs (bool): if true dirs will also be added to the results
-- opts.only_dirs (bool): if true only dirs will be added to the results
-- opts.respect_gitignore (bool): if true will only add files that are not ignored by the git (uses each gitignore found in path table)
-- opts.depth (int): depth on how deep the search should go
-- opts.search_pattern (regex): regex for which files will be added, string, table of strings, or callback (should return bool)
-- opts.on_insert(entry): Will be called for each element
-- opts.silent (bool): if true will not echo messages that are not accessible
-- @return array with files
m.scan_dir = function(path, opts)
opts = opts or {}
local data = {}
local base_paths = vim.tbl_flatten { path }
local next_dir = vim.tbl_flatten { path }
local gitignore = opts.respect_gitignore and make_gitignore(base_paths) or nil
local match_search_pat = opts.search_pattern and gen_search_pat(opts.search_pattern) or nil
for i = table.getn(base_paths), 1, -1 do
if uv.fs_access(base_paths[i], "X") == false then
if not F.if_nil(opts.silent, false, opts.silent) then
print(string.format("%s is not accessible by the current user!", base_paths[i]))
end
table.remove(base_paths, i)
end
end
if table.getn(base_paths) == 0 then
return {}
end
repeat
local current_dir = table.remove(next_dir, 1)
local fd = uv.fs_scandir(current_dir)
if fd then
while true do
local name, typ = uv.fs_scandir_next(fd)
if name == nil then
break
end
process_item(opts, name, typ, current_dir, next_dir, base_paths, data, gitignore, match_search_pat)
end
end
until table.getn(next_dir) == 0
return data
end
--- m.scan_dir_async
-- Search directory recursive and asyncronous
-- @param path: string or table
-- string has to be a valid path
-- table has to be a array of valid paths
-- @param opts: table to change behavior
-- opts.hidden (bool): if true hidden files will be added
-- opts.add_dirs (bool): if true dirs will also be added to the results
-- opts.only_dirs (bool): if true only dirs will be added to the results
-- opts.respect_gitignore (bool): if true will only add files that are not ignored by git
-- opts.depth (int): depth on how deep the search should go
-- opts.search_pattern (regex): regex for which files will be added, string, table of strings, or callback (should return bool)
-- opts.on_insert function(entry): will be called for each element
-- opts.on_exit function(results): will be called at the end
-- opts.silent (bool): if true will not echo messages that are not accessible
m.scan_dir_async = function(path, opts)
opts = opts or {}
local data = {}
local base_paths = vim.tbl_flatten { path }
local next_dir = vim.tbl_flatten { path }
local current_dir = table.remove(next_dir, 1)
-- TODO(conni2461): get gitignore is not async
local gitignore = opts.respect_gitignore and make_gitignore(base_paths) or nil
local match_search_pat = opts.search_pattern and gen_search_pat(opts.search_pattern) or nil
-- TODO(conni2461): is not async. Shouldn't be that big of a problem but still
-- Maybe obers async pr can take me out of callback hell
for i = table.getn(base_paths), 1, -1 do
if uv.fs_access(base_paths[i], "X") == false then
if not F.if_nil(opts.silent, false, opts.silent) then
print(string.format("%s is not accessible by the current user!", base_paths[i]))
end
table.remove(base_paths, i)
end
end
if table.getn(base_paths) == 0 then
return {}
end
local read_dir
read_dir = function(err, fd)
if not err then
while true do
local name, typ = uv.fs_scandir_next(fd)
if name == nil then
break
end
process_item(opts, name, typ, current_dir, next_dir, base_paths, data, gitignore, match_search_pat)
end
if table.getn(next_dir) == 0 then
if opts.on_exit then
opts.on_exit(data)
end
else
current_dir = table.remove(next_dir, 1)
uv.fs_scandir(current_dir, read_dir)
end
end
end
uv.fs_scandir(current_dir, read_dir)
end
local gen_permissions = (function()
local conv_to_octal = function(nr)
local octal, i = 0, 1
while nr ~= 0 do
octal = octal + (nr % 8) * i
nr = math.floor(nr / 8)
i = i * 10
end
return octal
end
local type_tbl = { [1] = "p", [2] = "c", [4] = "d", [6] = "b", [10] = ".", [12] = "l", [14] = "s" }
local permissions_tbl = { [0] = "---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx" }
local bit_tbl = { 4, 2, 1 }
return function(cache, mode)
if cache[mode] then
return cache[mode]
end
local octal = string.format("%6d", conv_to_octal(mode))
local l4 = octal:sub(#octal - 3, -1)
local bit = tonumber(l4:sub(1, 1))
local result = type_tbl[tonumber(octal:sub(1, 2))] or "-"
for i = 2, #l4 do
result = result .. permissions_tbl[tonumber(l4:sub(i, i))]
if bit - bit_tbl[i - 1] >= 0 then
result = result:sub(1, -2) .. (bit_tbl[i - 1] == 1 and "T" or "S")
bit = bit - bit_tbl[i - 1]
end
end
cache[mode] = result
return result
end
end)()
local gen_size = (function()
local size_types = { "", "K", "M", "G", "T", "P", "E", "Z" }
return function(size)
-- TODO(conni2461): If type directory we could just return 4.0K
for _, v in ipairs(size_types) do
if math.abs(size) < 1024.0 then
if math.abs(size) > 9 then
return string.format("%3d%s", size, v)
else
return string.format("%3.1f%s", size, v)
end
end
size = size / 1024.0
end
return string.format("%.1f%s", size, "Y")
end
end)()
local gen_date = (function()
local current_year = os.date "%Y"
return function(mtime)
if current_year ~= os.date("%Y", mtime) then
return os.date("%b %d %Y", mtime)
end
return os.date("%b %d %H:%M", mtime)
end
end)()
local get_username = (function()
if jit and os_sep ~= "\\" then
local ffi = require "ffi"
ffi.cdef [[
typedef unsigned int __uid_t;
typedef __uid_t uid_t;
typedef unsigned int __gid_t;
typedef __gid_t gid_t;
typedef struct {
char *pw_name;
char *pw_passwd;
__uid_t pw_uid;
__gid_t pw_gid;
char *pw_gecos;
char *pw_dir;
char *pw_shell;
} passwd;
passwd *getpwuid(uid_t uid);
]]
return function(tbl, id)
if tbl[id] then
return tbl[id]
end
local struct = ffi.C.getpwuid(id)
local name
if struct == nil then
name = tostring(id)
else
name = ffi.string(struct.pw_name)
end
tbl[id] = name
return name
end
else
return function(tbl, id)
if not tbl then
return id
end
if tbl[id] then
return tbl[id]
end
tbl[id] = tostring(id)
return id
end
end
end)()
local get_groupname = (function()
if jit and os_sep ~= "\\" then
local ffi = require "ffi"
ffi.cdef [[
typedef unsigned int __gid_t;
typedef __gid_t gid_t;
typedef struct {
char *gr_name;
char *gr_passwd;
__gid_t gr_gid;
char **gr_mem;
} group;
group *getgrgid(gid_t gid);
]]
return function(tbl, id)
if tbl[id] then
return tbl[id]
end
local struct = ffi.C.getgrgid(id)
local name
if struct == nil then
name = tostring(id)
else
name = ffi.string(struct.gr_name)
end
tbl[id] = name
return name
end
else
return function(tbl, id)
if not tbl then
return id
end
if tbl[id] then
return tbl[id]
end
tbl[id] = tostring(id)
return id
end
end
end)()
local get_max_len = function(tbl)
if not tbl then
return 0
end
local max_len = 0
for _, v in pairs(tbl) do
if #v > max_len then
max_len = #v
end
end
return max_len
end
local gen_ls = function(data, path, opts)
if not data or table.getn(data) == 0 then
return {}, {}
end
local check_link = function(per, file)
if per:sub(1, 1) == "l" then
local resolved = uv.fs_realpath(path .. os_sep .. file)
if not resolved then
return file
end
if resolved:sub(1, #path) == path then
resolved = resolved:sub(#path + 2, -1)
end
return string.format("%s -> %s", file, resolved)
end
return file
end
local results, sections = {}, {}
local users_tbl = os_sep ~= "\\" and {} or nil
local groups_tbl = os_sep ~= "\\" and {} or nil
local stats, permissions_cache = {}, {}
for _, v in ipairs(data) do
local stat = uv.fs_lstat(v)
if stat then
stats[v] = stat
get_username(users_tbl, stat.uid)
get_groupname(groups_tbl, stat.gid)
end
end
local insert_in_results = (function()
if not users_tbl and not groups_tbl then
local section_spacing_tbl = { [5] = 2, [6] = 0 }
return function(...)
local args = { ... }
local section = {
{ start_index = 01, end_index = 11 }, -- permissions, hardcoded indexes
{ start_index = 12, end_index = 17 }, -- size, hardcoded indexes
}
local cur_index = 19
for k = 5, 6 do
local v = section_spacing_tbl[k]
local end_index = cur_index + #args[k]
table.insert(section, { start_index = cur_index, end_index = end_index })
cur_index = end_index + v
end
table.insert(sections, section)
table.insert(
results,
string.format("%10s %5s %s %s", args[1], args[2], args[5], check_link(args[1], args[6]))
)
end
else
local max_user_len = get_max_len(users_tbl)
local max_group_len = get_max_len(groups_tbl)
local section_spacing_tbl = {
[3] = { max = max_user_len, add = 1 },
[4] = { max = max_group_len, add = 2 },
[5] = { add = 2 },
[6] = { add = 0 },
}
local fmt_str = "%10s %5s %-" .. max_user_len .. "s %-" .. max_group_len .. "s %s %s"
return function(...)
local args = { ... }
local section = {
{ start_index = 01, end_index = 11 }, -- permissions, hardcoded indexes
{ start_index = 12, end_index = 17 }, -- size, hardcoded indexes
}
local cur_index = 18
for k = 3, 6 do
local v = section_spacing_tbl[k]
local end_index = cur_index + #args[k]
table.insert(section, { start_index = cur_index, end_index = end_index })
if v.max then
cur_index = cur_index + v.max + v.add
else
cur_index = end_index + v.add
end
end
table.insert(sections, section)
table.insert(
results,
string.format(fmt_str, args[1], args[2], args[3], args[4], args[5], check_link(args[1], args[6]))
)
end
end
end)()
for name, stat in pairs(stats) do
insert_in_results(
gen_permissions(permissions_cache, stat.mode),
gen_size(stat.size),
get_username(users_tbl, stat.uid),
get_groupname(groups_tbl, stat.gid),
gen_date(stat.mtime.sec),
name:sub(#path + 2, -1)
)
end
if opts and opts.group_directories_first then
local sorted_results = {}
local sorted_sections = {}
for k, v in ipairs(results) do
if v:sub(1, 1) == "d" then
table.insert(sorted_results, v)
table.insert(sorted_sections, sections[k])
end
end
for k, v in ipairs(results) do
if v:sub(1, 1) ~= "d" then
table.insert(sorted_results, v)
table.insert(sorted_sections, sections[k])
end
end
return sorted_results, sorted_sections
else
return results, sections
end
end
--- m.ls
-- List directory contents. Will always apply --long option. Use scan_dir for without --long
-- @param path: string
-- string has to be a valid path
-- @param opts: table to change behavior
-- opts.hidden (bool): if true hidden files will be added
-- opts.add_dirs (bool): if true dirs will also be added to the results, default: true
-- opts.respect_gitignore (bool): if true will only add files that are not ignored by git
-- opts.depth (int): depth on how deep the search should go, default: 1
-- opts.group_directories_first (bool): same as real ls
-- @return array with formatted output
m.ls = function(path, opts)
opts = opts or {}
opts.depth = opts.depth or 1
opts.add_dirs = opts.add_dirs or true
local data = m.scan_dir(path, opts)
return gen_ls(data, path, opts)
end
--- m.ls_async
-- List directory contents. Will always apply --long option. Use scan_dir for without --long
-- @param path: string
-- string has to be a valid path
-- @param opts: table to change behavior
-- opts.hidden (bool): if true hidden files will be added
-- opts.add_dirs (bool): if true dirs will also be added to the results, default: true
-- opts.respect_gitignore (bool): if true will only add files that are not ignored by git
-- opts.depth (int): depth on how deep the search should go, default: 1
-- opts.group_directories_first (bool): same as real ls
-- opts.on_exit function(results): will be called at the end (required)
m.ls_async = function(path, opts)
opts = opts or {}
opts.depth = opts.depth or 1
opts.add_dirs = opts.add_dirs or true
local opts_copy = vim.deepcopy(opts)
opts_copy.on_exit = function(data)
if opts.on_exit then
opts.on_exit(gen_ls(data, path, opts_copy))
end
end
m.scan_dir_async(path, opts_copy)
end
return m

View File

@ -0,0 +1,188 @@
local path = require("plenary.path").path
local M = {}
M.strdisplaywidth = (function()
if jit and path.sep ~= [[\]] then
local ffi = require "ffi"
ffi.cdef [[
typedef unsigned char char_u;
int linetabsize_col(int startcol, char_u *s);
]]
return function(str, col)
str = tostring(str)
local startcol = col or 0
local s = ffi.new("char[?]", #str + 1)
ffi.copy(s, str)
return ffi.C.linetabsize_col(startcol, s) - startcol
end
else
return function(str, col)
str = tostring(str)
if vim.in_fast_event() then
return #str - (col or 0)
end
return vim.fn.strdisplaywidth(str, col)
end
end
end)()
M.strcharpart = (function()
if jit and path.sep ~= [[\]] then
local ffi = require "ffi"
ffi.cdef [[
typedef unsigned char char_u;
int utf_ptr2len(const char_u *const p);
]]
local function utf_ptr2len(str)
local c_str = ffi.new("char[?]", #str + 1)
ffi.copy(c_str, str)
return ffi.C.utf_ptr2len(c_str)
end
return function(str, nchar, charlen)
local nbyte = 0
if nchar > 0 then
while nchar > 0 and nbyte < #str do
nbyte = nbyte + utf_ptr2len(str:sub(nbyte + 1))
nchar = nchar - 1
end
else
nbyte = nchar
end
local len = 0
if charlen then
while charlen > 0 and nbyte + len < #str do
local off = nbyte + len
if off < 0 then
len = len + 1
else
len = len + utf_ptr2len(str:sub(off + 1))
end
charlen = charlen - 1
end
else
len = #str - nbyte
end
if nbyte < 0 then
len = len + nbyte
nbyte = 0
elseif nbyte > #str then
nbyte = #str
end
if len < 0 then
len = 0
elseif nbyte + len > #str then
len = #str - nbyte
end
return str:sub(nbyte + 1, nbyte + len)
end
else
return function(str, nchar, charlen)
if vim.in_fast_event() then
return str:sub(nchar + 1, charlen)
end
return vim.fn.strcharpart(str, nchar, charlen)
end
end
end)()
local truncate = function(str, len, dots, direction)
if M.strdisplaywidth(str) <= len then
return str
end
local start = direction > 0 and 0 or str:len()
local current = 0
local result = ""
local len_of_dots = M.strdisplaywidth(dots)
local concat = function(a, b, dir)
if dir > 0 then
return a .. b
else
return b .. a
end
end
while true do
local part = M.strcharpart(str, start, 1)
current = current + M.strdisplaywidth(part)
if (current + len_of_dots) > len then
result = concat(result, dots, direction)
break
end
result = concat(result, part, direction)
start = start + direction
end
return result
end
M.truncate = function(str, len, dots, direction)
str = tostring(str) -- We need to make sure its an actually a string and not a number
dots = dots or ""
direction = direction or 1
if direction ~= 0 then
return truncate(str, len, dots, direction)
else
if M.strdisplaywidth(str) <= len then
return str
end
local len1 = math.floor((len + M.strdisplaywidth(dots)) / 2)
local s1 = truncate(str, len1, dots, 1)
local len2 = len - M.strdisplaywidth(s1) + M.strdisplaywidth(dots)
local s2 = truncate(str, len2, dots, -1)
return s1 .. s2:sub(dots:len() + 1)
end
end
M.align_str = function(string, width, right_justify)
local str_len = M.strdisplaywidth(string)
return right_justify and string.rep(" ", width - str_len) .. string or string .. string.rep(" ", width - str_len)
end
M.dedent = function(str, leave_indent)
-- Check each line and detect the minimum indent.
local indent
local info = {}
for line in str:gmatch "[^\n]*\n?" do
-- It matches '' for the last line.
if line ~= "" then
local chars, width
local line_indent = line:match "^[ \t]+"
if line_indent then
chars = #line_indent
width = M.strdisplaywidth(line_indent)
if not indent or width < indent then
indent = width
end
-- Ignore empty lines
elseif line ~= "\n" then
indent = 0
end
table.insert(info, { line = line, chars = chars, width = width })
end
end
-- Build up the result
leave_indent = leave_indent or 0
local result = {}
for _, i in ipairs(info) do
local line
if i.chars then
local content = i.line:sub(i.chars + 1)
local indent_width = i.width - indent + leave_indent
line = (" "):rep(indent_width) .. content
elseif i.line == "\n" then
line = "\n"
else
line = (" "):rep(leave_indent) .. i.line
end
table.insert(result, line)
end
return table.concat(result)
end
return M

View File

@ -0,0 +1,40 @@
local tbl = {}
function tbl.apply_defaults(original, defaults)
if original == nil then
original = {}
end
original = vim.deepcopy(original)
for k, v in pairs(defaults) do
if original[k] == nil then
original[k] = v
end
end
return original
end
function tbl.pack(...)
return { n = select("#", ...), ... }
end
function tbl.unpack(t, i, j)
return unpack(t, i or 1, j or t.n or #t)
end
---Freeze a table. A frozen table is not able to be modified.
---http://lua-users.org/wiki/ReadOnlyTables
---@param t table
---@return table
function tbl.freeze(t)
return setmetatable({}, {
__index = t,
__newindex = function()
error "Attempt to modify frozen table"
end,
})
end
return tbl

View File

@ -0,0 +1,202 @@
local Path = require "plenary.path"
local Job = require "plenary.job"
local f = require "plenary.functional"
local log = require "plenary.log"
local win_float = require "plenary.window.float"
local headless = require("plenary.nvim_meta").is_headless
local harness = {}
local print_output = vim.schedule_wrap(function(_, ...)
for _, v in ipairs { ... } do
io.stdout:write(tostring(v))
io.stdout:write "\n"
end
vim.cmd [[mode]]
end)
local get_nvim_output = function(job_id)
return vim.schedule_wrap(function(bufnr, ...)
if not vim.api.nvim_buf_is_valid(bufnr) then
return
end
for _, v in ipairs { ... } do
vim.api.nvim_chan_send(job_id, v .. "\r\n")
end
end)
end
function harness.test_directory_command(command)
local split_string = vim.split(command, " ")
local directory = table.remove(split_string, 1)
local opts = assert(loadstring("return " .. table.concat(split_string, " ")))()
return harness.test_directory(directory, opts)
end
function harness.test_directory(directory, opts)
print "Starting..."
opts = vim.tbl_deep_extend("force", {
winopts = { winblend = 3 },
sequential = false,
keep_going = true,
timeout = 50000,
}, opts or {})
local res = {}
if not headless then
res = win_float.percentage_range_window(0.95, 0.70, opts.winopts)
res.job_id = vim.api.nvim_open_term(res.bufnr, {})
vim.api.nvim_buf_set_keymap(res.bufnr, "n", "q", ":q<CR>", {})
vim.api.nvim_win_set_option(res.win_id, "winhl", "Normal:Normal")
vim.api.nvim_win_set_option(res.win_id, "conceallevel", 3)
vim.api.nvim_win_set_option(res.win_id, "concealcursor", "n")
if res.border_win_id then
vim.api.nvim_win_set_option(res.border_win_id, "winhl", "Normal:Normal")
end
if res.bufnr then
vim.api.nvim_buf_set_option(res.bufnr, "filetype", "PlenaryTestPopup")
end
vim.cmd "mode"
end
local outputter = headless and print_output or get_nvim_output(res.job_id)
local paths = harness._find_files_to_run(directory)
local path_len = #paths
local failure = false
local jobs = vim.tbl_map(function(p)
local args = {
"--headless",
"-c",
string.format('lua require("plenary.busted").run("%s")', p:absolute()),
}
if opts.minimal ~= nil then
table.insert(args, "--noplugin")
elseif opts.minimal_init ~= nil then
table.insert(args, "--noplugin")
table.insert(args, "-u")
table.insert(args, opts.minimal_init)
end
local job = Job:new {
command = vim.v.progpath,
args = args,
-- Can be turned on to debug
on_stdout = function(_, data)
if path_len == 1 then
outputter(res.bufnr, data)
end
end,
on_stderr = function(_, data)
if path_len == 1 then
outputter(res.bufnr, data)
end
end,
on_exit = vim.schedule_wrap(function(j_self, _, _)
if path_len ~= 1 then
outputter(res.bufnr, unpack(j_self:stderr_result()))
outputter(res.bufnr, unpack(j_self:result()))
end
vim.cmd "mode"
end),
}
job.nvim_busted_path = p.filename
return job
end, paths)
log.debug "Running..."
for i, j in ipairs(jobs) do
outputter(res.bufnr, "Scheduling: " .. j.nvim_busted_path)
j:start()
if opts.sequential then
log.debug("... Sequential wait for job number", i)
Job.join(j, opts.timeout)
log.debug("... Completed job number", i)
if j.code ~= 0 then
failure = true
if not opts.keep_going then
break
end
end
end
end
-- TODO: Probably want to let people know when we've completed everything.
if not headless then
return
end
if not opts.sequential then
table.insert(jobs, opts.timeout)
log.debug "... Parallel wait"
Job.join(unpack(jobs))
log.debug "... Completed jobs"
table.remove(jobs, table.getn(jobs))
failure = f.any(function(_, v)
return v.code ~= 0
end, jobs)
end
vim.wait(100)
if headless then
if failure then
return vim.cmd "1cq"
end
return vim.cmd "0cq"
end
end
function harness._find_files_to_run(directory)
local finder = Job:new {
command = "find",
args = { directory, "-type", "f", "-name", "*_spec.lua" },
}
return vim.tbl_map(Path.new, finder:sync())
end
function harness._run_path(test_type, directory)
local paths = harness._find_files_to_run(directory)
local bufnr = 0
local win_id = 0
for _, p in pairs(paths) do
print " "
print("Loading Tests For: ", p:absolute(), "\n")
local ok, _ = pcall(function()
dofile(p:absolute())
end)
if not ok then
print "Failed to load file"
end
end
harness:run(test_type, bufnr, win_id)
vim.cmd "qa!"
return paths
end
return harness

View File

@ -0,0 +1,3 @@
return {
rotate = require "plenary.vararg.rotate",
}

View File

@ -0,0 +1,83 @@
---@brief [[
---Do not edit this file, it was generated!
---Provides a function to rotate a lua vararg
---@brief ]]
local tbl = require "plenary.tbl"
local rotate_lookup = {}
rotate_lookup[0] = function()
return A0
end
rotate_lookup[1] = function(A0)
return A0
end
rotate_lookup[2] = function(A0, A1)
return A1, A0
end
rotate_lookup[3] = function(A0, A1, A2)
return A1, A2, A0
end
rotate_lookup[4] = function(A0, A1, A2, A3)
return A1, A2, A3, A0
end
rotate_lookup[5] = function(A0, A1, A2, A3, A4)
return A1, A2, A3, A4, A0
end
rotate_lookup[6] = function(A0, A1, A2, A3, A4, A5)
return A1, A2, A3, A4, A5, A0
end
rotate_lookup[7] = function(A0, A1, A2, A3, A4, A5, A6)
return A1, A2, A3, A4, A5, A6, A0
end
rotate_lookup[8] = function(A0, A1, A2, A3, A4, A5, A6, A7)
return A1, A2, A3, A4, A5, A6, A7, A0
end
rotate_lookup[9] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8)
return A1, A2, A3, A4, A5, A6, A7, A8, A0
end
rotate_lookup[10] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9)
return A1, A2, A3, A4, A5, A6, A7, A8, A9, A0
end
rotate_lookup[11] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10)
return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A0
end
rotate_lookup[12] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11)
return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A0
end
rotate_lookup[13] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12)
return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A0
end
rotate_lookup[14] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13)
return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A0
end
rotate_lookup[15] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14)
return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A0
end
local function rotate_n(first, ...)
local args = tbl.pack(...)
args[#args + 1] = first
return tbl.unpack(args)
end
local function rotate(nargs, ...)
return (rotate_lookup[nargs] or rotate_n)(...)
end
return rotate

View File

@ -0,0 +1,297 @@
local strings = require "plenary.strings"
local Border = {}
Border.__index = Border
Border._default_thickness = {
top = 1,
right = 1,
bot = 1,
left = 1,
}
local calc_left_start = function(title_pos, title_len, total_width)
if string.find(title_pos, "W") then
return 0
elseif string.find(title_pos, "E") then
return total_width - title_len
else
return math.floor((total_width - title_len) / 2)
end
end
local create_horizontal_line = function(title, pos, width, left_char, mid_char, right_char)
local title_len
if title == "" then
title_len = 0
else
local len = strings.strdisplaywidth(title)
local max_title_width = width - 2
if len > max_title_width then
title = strings.truncate(title, max_title_width)
len = strings.strdisplaywidth(title)
end
title = string.format(" %s ", title)
title_len = len + 2
end
local left_start = calc_left_start(pos, title_len, width)
local horizontal_line = string.format(
"%s%s%s%s%s",
left_char,
string.rep(mid_char, left_start),
title,
string.rep(mid_char, width - title_len - left_start),
right_char
)
local ranges = {}
if title_len ~= 0 then
-- Need to calculate again due to multi-byte characters
local r_start = string.len(left_char) + math.max(left_start, 0) * string.len(mid_char)
ranges = { { r_start, r_start + string.len(title) } }
end
return horizontal_line, ranges
end
function Border._create_lines(content_win_id, content_win_options, border_win_options)
local content_pos = vim.api.nvim_win_get_position(content_win_id)
local content_height = vim.api.nvim_win_get_height(content_win_id)
local content_width = vim.api.nvim_win_get_width(content_win_id)
-- TODO: Handle border width, which I haven't right here.
local thickness = border_win_options.border_thickness
local top_enabled = thickness.top == 1
local right_enabled = thickness.right == 1 and content_pos[2] + content_width < vim.o.columns
local bot_enabled = thickness.bot == 1
local left_enabled = thickness.left == 1 and content_pos[2] > 0
border_win_options.border_thickness.left = left_enabled and 1 or 0
border_win_options.border_thickness.right = right_enabled and 1 or 0
local border_lines = {}
local ranges = {}
-- border_win_options.title should have be a list with entries of the
-- form: { pos = foo, text = bar }.
-- pos can take values in { "NW", "N", "NE", "SW", "S", "SE" }
local titles = type(border_win_options.title) == "string" and { { pos = "N", text = border_win_options.title } }
or border_win_options.title
or {}
local topline = nil
local topleft = (left_enabled and border_win_options.topleft) or ""
local topright = (right_enabled and border_win_options.topright) or ""
-- Only calculate the topline if there is space above the first content row (relative to the editor)
if content_pos[1] > 0 then
for _, title in ipairs(titles) do
if string.find(title.pos, "N") then
local top_ranges
topline, top_ranges = create_horizontal_line(
title.text,
title.pos,
content_win_options.width,
topleft,
border_win_options.top or "",
topright
)
for _, r in pairs(top_ranges) do
table.insert(ranges, { 0, r[1], r[2] })
end
break
end
end
if topline == nil then
if top_enabled then
topline = topleft .. string.rep(border_win_options.top, content_win_options.width) .. topright
end
end
else
border_win_options.border_thickness.top = 0
end
if topline then
table.insert(border_lines, topline)
end
local middle_line = string.format(
"%s%s%s",
(left_enabled and border_win_options.left) or "",
string.rep(" ", content_win_options.width),
(right_enabled and border_win_options.right) or ""
)
for _ = 1, content_win_options.height do
table.insert(border_lines, middle_line)
end
local botline = nil
local botleft = (left_enabled and border_win_options.botleft) or ""
local botright = (right_enabled and border_win_options.botright) or ""
if content_pos[1] + content_height < vim.o.lines then
for _, title in ipairs(titles) do
if string.find(title.pos, "S") then
local bot_ranges
botline, bot_ranges = create_horizontal_line(
title.text,
title.pos,
content_win_options.width,
botleft,
border_win_options.bot or "",
botright
)
for _, r in pairs(bot_ranges) do
table.insert(ranges, { content_win_options.height + thickness.top, r[1], r[2] })
end
break
end
end
if botline == nil then
if bot_enabled then
botline = botleft .. string.rep(border_win_options.bot, content_win_options.width) .. botright
end
end
else
border_win_options.border_thickness.bot = 0
end
if botline then
table.insert(border_lines, botline)
end
return border_lines, ranges
end
local set_title_highlights = function(bufnr, ranges, hl)
-- Check if both `hl` and `ranges` are provided, and `ranges` is not the empty table.
if hl and ranges and next(ranges) then
for _, r in pairs(ranges) do
vim.api.nvim_buf_add_highlight(bufnr, -1, hl, r[1], r[2], r[3])
end
end
end
function Border:change_title(new_title, pos)
if self._border_win_options.title == new_title then
return
end
pos = pos
or (self._border_win_options.title and self._border_win_options.title[1] and self._border_win_options.title[1].pos)
if pos == nil then
self._border_win_options.title = new_title
else
self._border_win_options.title = { { text = new_title, pos = pos } }
end
self.contents, self.title_ranges = Border._create_lines(
self.content_win_id,
self.content_win_options,
self._border_win_options
)
vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, self.contents)
set_title_highlights(self.bufnr, self.title_ranges, self._border_win_options.titlehighlight)
end
-- Updates characters for border lines, and returns nvim_win_config
-- (generally used in conjunction with `move` or `new`)
function Border:__align_calc_config(content_win_options, border_win_options)
border_win_options = vim.tbl_deep_extend("keep", border_win_options, {
border_thickness = Border._default_thickness,
-- Border options, could be passed as a list?
topleft = "",
topright = "",
top = "",
left = "",
right = "",
botleft = "",
botright = "",
bot = "",
})
-- Ensure the relevant contents and border win_options are set
self._border_win_options = border_win_options
self.content_win_options = content_win_options
-- Update border characters and title_ranges
self.contents, self.title_ranges = Border._create_lines(self.content_win_id, content_win_options, border_win_options)
vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, self.contents)
local thickness = border_win_options.border_thickness
local nvim_win_config = {
anchor = content_win_options.anchor,
relative = content_win_options.relative,
style = "minimal",
row = content_win_options.row - thickness.top,
col = content_win_options.col - thickness.left,
width = content_win_options.width + thickness.left + thickness.right,
height = content_win_options.height + thickness.top + thickness.bot,
zindex = content_win_options.zindex or 50,
noautocmd = content_win_options.noautocmd,
focusable = vim.F.if_nil(border_win_options.focusable, false),
}
return nvim_win_config
end
-- Sets the size and position of the given Border.
-- Can be used to create a new window (with `create_window = true`)
-- or change an existing one
function Border:move(content_win_options, border_win_options)
-- Update lines in border buffer, and get config for border window
local nvim_win_config = self:__align_calc_config(content_win_options, border_win_options)
-- Set config for border window
vim.api.nvim_win_set_config(self.win_id, nvim_win_config)
set_title_highlights(self.bufnr, self.title_ranges, self._border_win_options.titlehighlight)
end
function Border:new(content_bufnr, content_win_id, content_win_options, border_win_options)
assert(type(content_win_id) == "number", "Must supply a valid win_id. It's possible you forgot to call with ':'")
local obj = {}
obj.content_win_id = content_win_id
obj.bufnr = vim.api.nvim_create_buf(false, true)
assert(obj.bufnr, "Failed to create border buffer")
vim.api.nvim_buf_set_option(obj.bufnr, "bufhidden", "wipe")
-- Create a border window and buffer, with border characters around the edge
local nvim_win_config = Border.__align_calc_config(obj, content_win_options, border_win_options)
obj.win_id = vim.api.nvim_open_win(obj.bufnr, false, nvim_win_config)
if border_win_options.highlight then
vim.api.nvim_win_set_option(obj.win_id, "winhl", border_win_options.highlight)
end
set_title_highlights(obj.bufnr, obj.title_ranges, obj._border_win_options.titlehighlight)
vim.cmd(
string.format(
"autocmd BufDelete <buffer=%s> ++nested ++once :lua require('plenary.window').close_related_win(%s, %s)",
content_bufnr,
content_win_id,
obj.win_id
)
)
vim.cmd(
string.format(
"autocmd WinClosed <buffer=%s> ++nested ++once :lua require('plenary.window').try_close(%s, true)",
content_bufnr,
obj.win_id
)
)
setmetatable(obj, Border)
return obj
end
return Border

View File

@ -0,0 +1,212 @@
local Border = require "plenary.window.border"
local tbl = require "plenary.tbl"
_AssociatedBufs = {}
local clear_buf_on_leave = function(bufnr)
vim.cmd(
string.format(
"autocmd WinLeave,BufLeave,BufDelete <buffer=%s> ++once ++nested lua require('plenary.window.float').clear(%s)",
bufnr,
bufnr
)
)
end
local win_float = {}
win_float.default_options = {
winblend = 15,
percentage = 0.9,
}
function win_float.default_opts(options)
options = tbl.apply_defaults(options, win_float.default_options)
local width = math.floor(vim.o.columns * options.percentage)
local height = math.floor(vim.o.lines * options.percentage)
local top = math.floor(((vim.o.lines - height) / 2) - 1)
local left = math.floor((vim.o.columns - width) / 2)
local opts = {
relative = "editor",
row = top,
col = left,
width = width,
height = height,
style = "minimal",
}
return opts
end
function win_float.centered(options)
options = tbl.apply_defaults(options, win_float.default_options)
local win_opts = win_float.default_opts(options)
local bufnr = options.bufnr or vim.api.nvim_create_buf(false, true)
local win_id = vim.api.nvim_open_win(bufnr, true, win_opts)
vim.cmd "setlocal nocursorcolumn"
vim.api.nvim_win_set_option(win_id, "winblend", options.winblend)
vim.cmd(string.format("autocmd WinLeave <buffer> silent! execute 'bdelete! %s'", bufnr))
return {
bufnr = bufnr,
win_id = win_id,
}
end
function win_float.centered_with_top_win(top_text, options)
options = tbl.apply_defaults(options, win_float.default_options)
table.insert(top_text, 1, string.rep("=", 80))
table.insert(top_text, string.rep("=", 80))
local primary_win_opts = win_float.default_opts(nil, nil, options)
local minor_win_opts = vim.deepcopy(primary_win_opts)
primary_win_opts.height = primary_win_opts.height - #top_text - 1
primary_win_opts.row = primary_win_opts.row + #top_text + 1
minor_win_opts.height = #top_text
local minor_bufnr = vim.api.nvim_create_buf(false, true)
local minor_win_id = vim.api.nvim_open_win(minor_bufnr, true, minor_win_opts)
vim.cmd "setlocal nocursorcolumn"
vim.api.nvim_win_set_option(minor_win_id, "winblend", options.winblend)
vim.api.nvim_buf_set_lines(minor_bufnr, 0, -1, false, top_text)
local primary_bufnr = vim.api.nvim_create_buf(false, true)
local primary_win_id = vim.api.nvim_open_win(primary_bufnr, true, primary_win_opts)
vim.cmd "setlocal nocursorcolumn"
vim.api.nvim_win_set_option(primary_win_id, "winblend", options.winblend)
-- vim.cmd(
-- string.format(
-- "autocmd WinLeave,BufDelete,BufLeave <buffer=%s> ++once ++nested silent! execute 'bdelete! %s'",
-- primary_buf,
-- minor_buf
-- )
-- )
-- vim.cmd(
-- string.format(
-- "autocmd WinLeave,BufDelete,BufLeave <buffer> ++once ++nested silent! execute 'bdelete! %s'",
-- primary_buf
-- )
-- )
local primary_border = Border:new(primary_bufnr, primary_win_id, primary_win_opts, {})
local minor_border = Border:new(minor_bufnr, minor_win_id, minor_win_opts, {})
_AssociatedBufs[primary_bufnr] = {
primary_win_id,
minor_win_id,
primary_border.win_id,
minor_border.win_id,
}
clear_buf_on_leave(primary_bufnr)
return {
bufnr = primary_bufnr,
win_id = primary_win_id,
minor_bufnr = minor_bufnr,
minor_win_id = minor_win_id,
}
end
--- Create window that takes up certain percentags of the current screen.
---
--- Works regardless of current buffers, tabs, splits, etc.
--@param col_range number | Table:
-- If number, then center the window taking up this percentage of the screen.
-- If table, first index should be start, second_index should be end
--@param row_range number | Table:
-- If number, then center the window taking up this percentage of the screen.
-- If table, first index should be start, second_index should be end
--@param win_opts Table
--@param border_opts Table
function win_float.percentage_range_window(col_range, row_range, win_opts, border_opts)
win_opts = tbl.apply_defaults(win_opts, win_float.default_options)
local default_win_opts = win_float.default_opts(win_opts)
default_win_opts.relative = "editor"
local height_percentage, row_start_percentage
if type(row_range) == "number" then
assert(row_range <= 1)
assert(row_range > 0)
height_percentage = row_range
row_start_percentage = (1 - height_percentage) / 2
elseif type(row_range) == "table" then
height_percentage = row_range[2] - row_range[1]
row_start_percentage = row_range[1]
else
error(string.format("Invalid type for 'row_range': %p", row_range))
end
default_win_opts.height = math.ceil(vim.o.lines * height_percentage)
default_win_opts.row = math.ceil(vim.o.lines * row_start_percentage)
local width_percentage, col_start_percentage
if type(col_range) == "number" then
assert(col_range <= 1)
assert(col_range > 0)
width_percentage = col_range
col_start_percentage = (1 - width_percentage) / 2
elseif type(col_range) == "table" then
width_percentage = col_range[2] - col_range[1]
col_start_percentage = col_range[1]
else
error(string.format("Invalid type for 'col_range': %p", col_range))
end
default_win_opts.col = math.floor(vim.o.columns * col_start_percentage)
default_win_opts.width = math.floor(vim.o.columns * width_percentage)
local bufnr = win_opts.bufnr or vim.api.nvim_create_buf(false, true)
local win_id = vim.api.nvim_open_win(bufnr, true, default_win_opts)
vim.api.nvim_win_set_buf(win_id, bufnr)
vim.cmd "setlocal nocursorcolumn"
vim.api.nvim_win_set_option(win_id, "winblend", win_opts.winblend)
local border = Border:new(bufnr, win_id, default_win_opts, border_opts or {})
_AssociatedBufs[bufnr] = { win_id, border.win_id }
clear_buf_on_leave(bufnr)
return {
bufnr = bufnr,
win_id = win_id,
border_bufnr = border.bufnr,
border_win_id = border.win_id,
}
end
function win_float.clear(bufnr)
if _AssociatedBufs[bufnr] == nil then
return
end
for _, win_id in ipairs(_AssociatedBufs[bufnr]) do
if vim.api.nvim_win_is_valid(win_id) then
vim.api.nvim_win_close(win_id, true)
end
end
_AssociatedBufs[bufnr] = nil
end
return win_float

View File

@ -0,0 +1,16 @@
local window = {}
window.try_close = function(win_id, force)
if force == nil then
force = true
end
pcall(vim.api.nvim_win_close, win_id, force)
end
window.close_related_win = function(parent_win_id, child_win_id)
window.try_close(parent_win_id, true)
window.try_close(child_win_id, true)
end
return window

View File

@ -0,0 +1,61 @@
local unpack = table.unpack or unpack
local registry = { }
local current_namespace
local fallback_namespace
local s = {
_COPYRIGHT = "Copyright (c) 2012 Olivine Labs, LLC.",
_DESCRIPTION = "A simple string key/value store for i18n or any other case where you want namespaced strings.",
_VERSION = "Say 1.2",
set_namespace = function(self, namespace)
current_namespace = namespace
if not registry[current_namespace] then
registry[current_namespace] = {}
end
end,
set_fallback = function(self, namespace)
fallback_namespace = namespace
if not registry[fallback_namespace] then
registry[fallback_namespace] = {}
end
end,
set = function(self, key, value)
registry[current_namespace][key] = value
end
}
local __meta = {
__call = function(self, key, vars)
vars = vars or {}
local str = registry[current_namespace][key] or registry[fallback_namespace][key]
if str == nil then
return nil
end
str = tostring(str)
local strings = {}
for i,v in ipairs(vars) do
table.insert(strings, tostring(v))
end
return #strings > 0 and str:format(unpack(strings)) or str
end,
__index = function(self, key)
return registry[key]
end
}
s:set_fallback('en')
s:set_namespace('en')
s._registry = registry
return setmetatable(s, __meta)

View File

@ -0,0 +1,103 @@
local _MODREV, _SPECREV = 'scm', '-1'
rockspec_format = "3.0"
package = 'plenary.nvim'
version = _MODREV .. _SPECREV
description = {
summary = 'lua functions you don\'t want to write ',
labels = { "neovim" },
detailed = [[
plenary: full; complete; entire; absolute; unqualified. All the lua functions I don't want to write twice.
]],
homepage = 'http://github.com/nvim-lua/plenary.nvim',
license = 'MIT/X11',
}
dependencies = {
'lua >= 5.1, < 5.4',
'luassert'
}
source = {
url = 'http://github.com/nvim-lua/plenary.nvim/archive/v' .. _MODREV .. '.zip',
dir = 'plenary.nvim-' .. _MODREV,
}
if _MODREV == 'scm' then
source = {
url = 'git://github.com/nvim-lua/plenary.nvim',
}
end
build = {
type = 'builtin',
modules = {
-- paths are relative to source.dir
["plenary.busted"] = "lua/plenary/busted.lua",
["plenary.class"] = "lua/plenary/class.lua",
["plenary.context_manager"] = "lua/plenary/context_manager.lua",
["plenary.debug_utils"] = "lua/plenary/debug_utils.lua",
["plenary.enum"] = "lua/plenary/enum.lua",
["plenary.errors"] = "lua/plenary/errors.lua",
["plenary.filetype"] = "lua/plenary/filetype.lua",
["plenary.fun"] = "lua/plenary/fun.lua",
["plenary.functional"] = "lua/plenary/functional.lua",
["plenary.init"] = "lua/plenary/init.lua",
["plenary.iterators"] = "lua/plenary/iterators.lua",
["plenary.job"] = "lua/plenary/job.lua",
["plenary.log"] = "lua/plenary/log.lua",
["plenary.nvim_meta"] = "lua/plenary/nvim_meta.lua",
["plenary.operators"] = "lua/plenary/operators.lua",
["plenary.path"] = "lua/plenary/path.lua",
["plenary.profile"] = "lua/plenary/profile.lua",
["plenary.reload"] = "lua/plenary/reload.lua",
["plenary.run"] = "lua/plenary/run.lua",
["plenary.scandir"] = "lua/plenary/scandir.lua",
["plenary.strings"] = "lua/plenary/strings.lua",
["plenary.tbl"] = "lua/plenary/tbl.lua",
["plenary.test_harness"] = "lua/plenary/test_harness.lua",
["plenary.async.api"] = "lua/plenary/async/api.lua",
["plenary.async.async"] = "lua/plenary/async/async.lua",
["plenary.async.control"] = "lua/plenary/async/control.lua",
["plenary.async.init"] = "lua/plenary/async/init.lua",
["plenary.async.lsp"] = "lua/plenary/async/lsp.lua",
["plenary.async.structs"] = "lua/plenary/async/structs.lua",
["plenary.async.tests"] = "lua/plenary/async/tests.lua",
["plenary.async.util"] = "lua/plenary/async/util.lua",
["plenary.async.uv_async"] = "lua/plenary/async/uv_async.lua",
["plenary.async_lib.api"] = "lua/plenary/async_lib/api.lua",
["plenary.async_lib.async"] = "lua/plenary/async_lib/async.lua",
["plenary.async_lib.init"] = "lua/plenary/async_lib/init.lua",
["plenary.async_lib.lsp"] = "lua/plenary/async_lib/lsp.lua",
["plenary.async_lib.structs"] = "lua/plenary/async_lib/structs.lua",
["plenary.async_lib.tests"] = "lua/plenary/async_lib/tests.lua",
["plenary.async_lib.util"] = "lua/plenary/async_lib/util.lua",
["plenary.async_lib.uv_async"] = "lua/plenary/async_lib/uv_async.lua",
["plenary.collections.py_list"] = "lua/plenary/collections/py_list.lua",
["plenary.lsp.override"] = "lua/plenary/lsp/override.lua",
["plenary.neorocks.init"] = "lua/plenary/neorocks/init.lua",
["plenary.popup.init"] = "lua/plenary/popup/init.lua",
["plenary.popup.utils"] = "lua/plenary/popup/utils.lua",
["plenary.profile.lua_profiler"] = "lua/plenary/profile/lua_profiler.lua",
["plenary.profile.memory_profiler"] = "lua/plenary/profile/memory_profiler.lua",
["plenary.profile.p"] = "lua/plenary/profile/p.lua",
["plenary.vararg.init"] = "lua/plenary/vararg/init.lua",
["plenary.vararg.rotate"] = "lua/plenary/vararg/rotate.lua",
["plenary.window.border"] = "lua/plenary/window/border.lua",
["plenary.window.float"] = "lua/plenary/window/float.lua",
["plenary.window.init"] = "lua/plenary/window/init.lua",
},
copy_directories = {
'plugin'
}
}

View File

@ -0,0 +1,9 @@
" Create command for running busted
command! -nargs=1 -complete=file PlenaryBustedFile
\ lua require('plenary.busted').run(vim.fn.expand("<args>"))
command! -nargs=+ -complete=file PlenaryBustedDirectory
\ lua require('plenary.test_harness').test_directory_command(vim.fn.expand("<args>"))
nnoremap <Plug>PlenaryTestFile :lua require('plenary.test_harness').test_directory(vim.fn.expand("%:p"))<CR>

View File

@ -0,0 +1,231 @@
-------------------------------------------------------------------------------
-- This module implements a function that traverses all live objects.
-- You can implement your own function to pass as a parameter of traverse
-- and give you the information you want. As an example we have implemented
-- countreferences and findallpaths
--
-- Alexandra Barros - 2006.03.15
-------------------------------------------------------------------------------
module("gc", package.seeall)
local List = {}
function List.new ()
return {first = 0, last = -1}
end
function List.push (list, value)
local last = list.last + 1
list.last = last
list[last] = value
end
function List.pop (list)
local first = list.first
if first > list.last then error("list is empty") end
local value = list[first]
list[first] = nil
list.first = first + 1
return value
end
function List.isempty (list)
return list.first > list.last
end
-- Counts all references for a given object
function countreferences(value)
local count = -1
local f = function(from, to, how, v)
if to == value then
count = count + 1
end
end
traverse({edge=f}, {count, f})
return count
end
-- Prints all paths to an object
function findallpaths(obj)
local comefrom = {}
local f = function(from, to, how, value)
if not comefrom[to] then comefrom[to] = {} end
table.insert(comefrom[to], 1, {f = from, h = how, v=value})
end
traverse({edge=f}, {comefrom, f})
local function printpath(to)
if not to or comefrom[to].visited or to == _G then
print("-----")
return
end
comefrom[to].visited = true
for i=1, #comefrom[to] do
local tfrom = comefrom[to][i].f
print("from: ", vim.inspect(tfrom, { newline = '|' }), "\nhow:", comefrom[to][i].h,
"\nvalue:", comefrom[to][i].v)
printpath(tfrom)
end
end
printpath(obj)
end
-- Main function
-- 'funcs' is a table that contains a funcation for every lua type and also the
-- function edge edge (traverseedge).
function traverse(funcs, ignoreobjs)
-- The keys of the marked table are the objetcts (for example, table: 00442330).
-- The value of each key is true if the object has been found and false
-- otherwise.
local env = {marked = {}, list=List.new(), funcs=funcs}
if ignoreobjs then
for i=1, #ignoreobjs do
env.marked[ignoreobjs[i]] = true
end
end
env.marked["gc"] = true
env.marked[gc] = true
-- marks and inserts on the list
edge(env, nil, "_G", "isname", nil)
edge(env, nil, _G, "key", "_G")
-- traverses the active thread
-- inserts the local variables
-- interates over the function on the stack, starting from the one that
-- called traverse
for i=2, math.huge do
local info = debug.getinfo(i, "f")
if not info then break end
for j=1, math.huge do
local n, v = debug.getlocal(i, j)
if not n then break end
edge(env, nil, n, "isname", nil)
edge(env, nil, v, "local", n)
end
end
while not List.isempty(env.list) do
local obj = List.pop(env.list)
local t = type(obj)
if not gc["traverse" .. t] then
error("Could not find traverse " .. t)
end
gc["traverse" .. t](env, obj)
end
end
function traversetable(env, obj)
local f = env.funcs.table
if f then f(obj) end
for key, value in pairs(obj) do
edge(env, obj, key, "iskey", nil)
edge(env, obj, value, "key", key)
end
local mtable = debug.getmetatable(obj)
if mtable then edge(env, obj, mtable, "ismetatable", nil) end
end
function traversestring(env, obj)
local f = env.funcs.string
if f then f(obj) end
end
function traverseuserdata(env, obj)
local f = env.funcs.userdata
if f then f(obj) end
local mtable = debug.getmetatable(obj)
if mtable then edge(env, obj, mtable, "ismetatable", nil) end
local fenv = debug.getfenv(obj)
if fenv then edge(env, obj, fenv, "environment", nil) end
end
function traversefunction(env, obj)
local f = env.funcs.func
if f then f(obj) end
-- gets the upvalues
local i = 1
while true do
local n, v = debug.getupvalue(obj, i)
if not n then break end -- when there is no upvalues
edge(env, obj, n, "isname", nil)
edge(env, obj, v, "upvalue", n)
i = i + 1
end
local fenv = debug.getfenv(obj)
edge(env, obj, fenv, "enviroment", nil)
end
function traversecdata(env, t)
-- print(env, t)
end
function traversethread(env, t)
local f = env.funcs.thread
if f then f(t) end
for i=1, math.huge do
local info = debug.getinfo(t, i, "f")
if not info then break end
for j=1, math.huge do
local n, v = debug.getlocal(t, i , j)
if not n then break end
print(n, v)
edge(env, nil, n, "isname", nil)
edge(env, nil, v, "local", n)
end
end
local fenv = debug.getfenv(t)
edge(env, t, fenv, "enviroment", nil)
end
-- 'how' is a string that identifies the content of 'to' and 'value':
-- if 'how' is "iskey", then 'to' é is a key and 'value' is nil.
-- if 'how' is "key", then 'to' is an object and 'value' is the name of the
-- key.
function edge(env, from, to, how, value)
local t = type(to)
if to and (t~="boolean") and (t~="number") and (t~="new") then
-- If the destination object has not been found yet
if not env.marked[to] then
env.marked[to] = true
List.push(env.list, to) -- puts on the list to be traversed
end
local f = env.funcs.edge
if f then f(from, to, how, value) end
end
end

View File

@ -0,0 +1,24 @@
local level = 2
local info = debug.getinfo(1, 'Sf')
local source = info.source
local b = require('busted.core')()
b.register('file', 'file', {})
local testFileLoader = require 'busted.modules.standalone_loader'(b)
testFileLoader(info, { verbose = nil })
local execute = require('busted.execute')(b)
print(vim.inspect(execute))
print(vim.inspect(b))
print("===")
print(execute(1, {}))
print("===")
-- execute(1, {})
-- print(vim.inspect(b.execute))
-- print(vim.inspect(b.execute('file', './tests/plenary/bu/simple_busted_spec.lua')))
-- require('busted.init')(b)
-- print(vim.inspect(b))

View File

@ -0,0 +1,14 @@
local f = function(xyz)
print('calling 1', xyz)
local foo = coroutine.yield()
print('calling 2', foo)
return 5
end
local x = coroutine.wrap(f)
x('bar')
x('zap')
-- x()
-- x()

View File

@ -0,0 +1,4 @@
local i = require('plenary.iterators')
dump(i.split(" hello person dude ", " "):tolist())
-- dump(("hello person"):find("person"))

View File

@ -0,0 +1,55 @@
function Job.accumulate_results(results)
return function(err, data)
if data == nil then
if results[#results] == '' then
table.remove(results, #results)
end
return
end
if results[1] == nil then
results[1] = ''
end
-- Get rid of pesky \r
data = data:gsub("\r", "")
local line, start, found_newline
while true do
start = string.find(data, "\n") or #data
found_newline = string.find(data, "\n")
line = string.sub(data, 1, start)
data = string.sub(data, start + 1, -1)
line = line:gsub("\r", "")
line = line:gsub("\n", "")
results[#results] = (results[#results] or '') .. line
if found_newline then
table.insert(results, '')
else
break
end
end
-- if found_newline and results[#results] == '' then
-- table.remove(results, #results)
-- end
-- if string.find(data, "\n") then
-- for _, line in ipairs(vim.fn.split(data, "\n")) do
-- line = line:gsub("\n", "")
-- line = line:gsub("\r", "")
-- table.insert(results, line)
-- end
-- else
-- results[#results] = results[#results] .. data
-- end
end
end

Some files were not shown because too many files have changed in this diff Show More